diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index 54a04197a..e86ee07a1 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -29,9 +29,12 @@ 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.StorageManager; -import com.fsck.k9.mail.store.StorageManager.StorageProvider; -import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mail.filter.Base64; +import com.fsck.k9.mail.store.RemoteStore; +import com.fsck.k9.mail.store.StoreConfig; +import com.fsck.k9.mailstore.StorageManager; +import com.fsck.k9.mailstore.StorageManager.StorageProvider; +import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.StatsColumns; import com.fsck.k9.search.ConditionsTreeNode; @@ -40,7 +43,7 @@ import com.fsck.k9.search.SqlQueryBuilder; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchCondition; import com.fsck.k9.search.SearchSpecification.Searchfield; -import com.fsck.k9.security.LocalKeyStore; +import com.fsck.k9.mail.ssl.LocalKeyStore; import com.fsck.k9.view.ColorChip; import com.larswerkman.colorpicker.ColorPicker; @@ -48,7 +51,7 @@ import com.larswerkman.colorpicker.ColorPicker; * Account stores all of the settings for a single account defined by the user. It is able to save * and delete itself given a Preferences to work with. Each account is defined by a UUID. */ -public class Account implements BaseAccount { +public class Account implements BaseAccount, StoreConfig { /** * Default value for the inbox folder (never changes for POP3 and IMAP) */ @@ -361,9 +364,9 @@ public class Account implements BaseAccount { SharedPreferences prefs = preferences.getPreferences(); - mStoreUri = Utility.base64Decode(prefs.getString(mUuid + ".storeUri", null)); + mStoreUri = Base64.decode(prefs.getString(mUuid + ".storeUri", null)); mLocalStorageProviderId = prefs.getString(mUuid + ".localStorageProvider", StorageManager.getInstance(K9.app).getDefaultProviderId()); - mTransportUri = Utility.base64Decode(prefs.getString(mUuid + ".transportUri", null)); + mTransportUri = Base64.decode(prefs.getString(mUuid + ".transportUri", null)); mDescription = prefs.getString(mUuid + ".description", null); mAlwaysBcc = prefs.getString(mUuid + ".alwaysBcc", mAlwaysBcc); mAutomaticCheckIntervalMinutes = prefs.getInt(mUuid + ".automaticCheckIntervalMinutes", -1); @@ -689,9 +692,9 @@ public class Account implements BaseAccount { editor.putString("accountUuids", accountUuids); } - editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri)); + editor.putString(mUuid + ".storeUri", Base64.encode(mStoreUri)); editor.putString(mUuid + ".localStorageProvider", mLocalStorageProviderId); - editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri)); + editor.putString(mUuid + ".transportUri", Base64.encode(mTransportUri)); editor.putString(mUuid + ".description", mDescription); editor.putString(mUuid + ".alwaysBcc", mAlwaysBcc); editor.putInt(mUuid + ".automaticCheckIntervalMinutes", mAutomaticCheckIntervalMinutes); @@ -1076,8 +1079,8 @@ public class Account implements BaseAccount { return mDraftsFolderName; } - public synchronized void setDraftsFolderName(String draftsFolderName) { - mDraftsFolderName = draftsFolderName; + public synchronized void setDraftsFolderName(String name) { + mDraftsFolderName = name; } /** @@ -1096,8 +1099,8 @@ public class Account implements BaseAccount { return K9.ERROR_FOLDER_NAME; } - public synchronized void setSentFolderName(String sentFolderName) { - mSentFolderName = sentFolderName; + public synchronized void setSentFolderName(String name) { + mSentFolderName = name; } /** @@ -1113,8 +1116,8 @@ public class Account implements BaseAccount { return mTrashFolderName; } - public synchronized void setTrashFolderName(String trashFolderName) { - mTrashFolderName = trashFolderName; + public synchronized void setTrashFolderName(String name) { + mTrashFolderName = name; } /** @@ -1145,8 +1148,8 @@ public class Account implements BaseAccount { return mSpamFolderName; } - public synchronized void setSpamFolderName(String spamFolderName) { - mSpamFolderName = spamFolderName; + public synchronized void setSpamFolderName(String name) { + mSpamFolderName = name; } /** @@ -1165,8 +1168,8 @@ public class Account implements BaseAccount { return mAutoExpandFolderName; } - public synchronized void setAutoExpandFolderName(String autoExpandFolderName) { - mAutoExpandFolderName = autoExpandFolderName; + public synchronized void setAutoExpandFolderName(String name) { + mAutoExpandFolderName = name; } public synchronized int getAccountNumber() { @@ -1289,11 +1292,11 @@ public class Account implements BaseAccount { } public LocalStore getLocalStore() throws MessagingException { - return Store.getLocalInstance(this, K9.app); + return LocalStore.getInstance(this, K9.app); } public Store getRemoteStore() throws MessagingException { - return Store.getRemoteInstance(this); + return RemoteStore.getInstance(this); } // It'd be great if this actually went into the store implementation @@ -1657,8 +1660,8 @@ public class Account implements BaseAccount { return mInboxFolderName; } - public void setInboxFolderName(String mInboxFolderName) { - this.mInboxFolderName = mInboxFolderName; + public void setInboxFolderName(String name) { + this.mInboxFolderName = name; } public synchronized boolean syncRemoteDeletions() { diff --git a/src/com/fsck/k9/EmailAddressValidator.java b/src/com/fsck/k9/EmailAddressValidator.java index 957a1384b..211d99220 100644 --- a/src/com/fsck/k9/EmailAddressValidator.java +++ b/src/com/fsck/k9/EmailAddressValidator.java @@ -4,7 +4,19 @@ package com.fsck.k9; import android.text.util.Rfc822Tokenizer; import android.widget.AutoCompleteTextView.Validator; +import java.util.regex.Pattern; + public class EmailAddressValidator implements Validator { + private static final Pattern EMAIL_ADDRESS_PATTERN = Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + ); + public CharSequence fixText(CharSequence invalidText) { return ""; } @@ -14,6 +26,6 @@ public class EmailAddressValidator implements Validator { } public boolean isValidAddressOnly(CharSequence text) { - return com.fsck.k9.helper.Regex.EMAIL_ADDRESS_PATTERN.matcher(text).matches(); + return EMAIL_ADDRESS_PATTERN.matcher(text).matches(); } } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 08c49c5ed..cc359a6ac 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -36,9 +36,9 @@ 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.local.LocalStore; +import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.provider.UnreadWidgetProvider; -import com.fsck.k9.security.LocalKeyStore; +import com.fsck.k9.mail.ssl.LocalKeyStore; import com.fsck.k9.service.BootReceiver; import com.fsck.k9.service.MailService; import com.fsck.k9.service.ShutdownReceiver; @@ -136,37 +136,6 @@ public class K9 extends Application { */ public static boolean DEBUG = false; - /** - * Should K-9 log the conversation it has over the wire with - * SMTP servers? - */ - - public static boolean DEBUG_PROTOCOL_SMTP = true; - - /** - * Should K-9 log the conversation it has over the wire with - * IMAP servers? - */ - - public static boolean DEBUG_PROTOCOL_IMAP = true; - - - /** - * Should K-9 log the conversation it has over the wire with - * POP3 servers? - */ - - public static boolean DEBUG_PROTOCOL_POP3 = true; - - /** - * Should K-9 log the conversation it has over the wire with - * WebDAV servers? - */ - - public static boolean DEBUG_PROTOCOL_WEBDAV = true; - - - /** * If this is enabled than logging that normally hides sensitive information * like passwords will show that information. diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index 5fd4929a3..03a3cd1be 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -13,7 +13,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; -import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.store.RemoteStore; +import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.preferences.Editor; import com.fsck.k9.preferences.Storage; @@ -121,7 +122,12 @@ public class Preferences { accountsInOrder.remove(account); } - Store.removeAccount(account); + try { + RemoteStore.removeInstance(account); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Failed to reset remote store for account " + account.getUuid(), e); + } + LocalStore.removeAccount(account); account.deleteCertificates(); account.delete(this); diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 738ee32a3..0a7041165 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -5,7 +5,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.EnumSet; import java.util.HashSet; @@ -76,13 +75,12 @@ 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; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mail.store.RemoteStore; +import com.fsck.k9.mailstore.StorageManager; import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.preferences.SettingsExporter; import com.fsck.k9.preferences.SettingsImportExportException; @@ -771,7 +769,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { } private void show(final Accounts activity, boolean restore) { - ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri()); + ServerSettings incoming = RemoteStore.decodeStoreUri(mAccount.getStoreUri()); ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri()); /* @@ -992,9 +990,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { if (mIncomingPassword != null) { // Set incoming server password String storeUri = mAccount.getStoreUri(); - ServerSettings incoming = Store.decodeStoreUri(storeUri); + ServerSettings incoming = RemoteStore.decodeStoreUri(storeUri); ServerSettings newIncoming = incoming.newPassword(mIncomingPassword); - String newStoreUri = Store.createStoreUri(newIncoming); + String newStoreUri = RemoteStore.createStoreUri(newIncoming); mAccount.setStoreUri(newStoreUri); } diff --git a/src/com/fsck/k9/activity/ChooseFolder.java b/src/com/fsck/k9/activity/ChooseFolder.java index 83e8070b8..91430796d 100644 --- a/src/com/fsck/k9/activity/ChooseFolder.java +++ b/src/com/fsck/k9/activity/ChooseFolder.java @@ -280,22 +280,16 @@ public class ChooseFolder extends K9ListActivity { mAccount.getInboxFolderName().equalsIgnoreCase(name)))) { continue; } - try { - folder.refresh(prefs); - Folder.FolderClass fMode = folder.getDisplayClass(); + Folder.FolderClass fMode = folder.getDisplayClass(); - if ((aMode == Account.FolderMode.FIRST_CLASS && - fMode != Folder.FolderClass.FIRST_CLASS) || ( - aMode == Account.FolderMode.FIRST_AND_SECOND_CLASS && - fMode != Folder.FolderClass.FIRST_CLASS && - fMode != Folder.FolderClass.SECOND_CLASS) || ( - aMode == Account.FolderMode.NOT_SECOND_CLASS && - fMode == Folder.FolderClass.SECOND_CLASS)) { - continue; - } - } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Couldn't get prefs to check for displayability of folder " + - folder.getName(), me); + if ((aMode == FolderMode.FIRST_CLASS && + fMode != Folder.FolderClass.FIRST_CLASS) || ( + aMode == FolderMode.FIRST_AND_SECOND_CLASS && + fMode != Folder.FolderClass.FIRST_CLASS && + fMode != Folder.FolderClass.SECOND_CLASS) || ( + aMode == FolderMode.NOT_SECOND_CLASS && + fMode == Folder.FolderClass.SECOND_CLASS)) { + continue; } if (folder.isInTopGroup()) { diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 670419cf1..254b87745 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -55,8 +55,7 @@ import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.store.local.LocalFolder; +import com.fsck.k9.mailstore.LocalFolder; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; @@ -749,20 +748,14 @@ public class FolderList extends K9ListActivity { Account.FolderMode aMode = account.getFolderDisplayMode(); Preferences prefs = Preferences.getPreferences(getApplication().getApplicationContext()); for (Folder folder : folders) { - try { - folder.refresh(prefs); + Folder.FolderClass fMode = folder.getDisplayClass(); - Folder.FolderClass fMode = folder.getDisplayClass(); - - if ((aMode == Account.FolderMode.FIRST_CLASS && fMode != Folder.FolderClass.FIRST_CLASS) - || (aMode == Account.FolderMode.FIRST_AND_SECOND_CLASS && - fMode != Folder.FolderClass.FIRST_CLASS && - fMode != Folder.FolderClass.SECOND_CLASS) - || (aMode == Account.FolderMode.NOT_SECOND_CLASS && fMode == Folder.FolderClass.SECOND_CLASS)) { - continue; - } - } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Couldn't get prefs to check for displayability of folder " + folder.getName(), me); + if ((aMode == FolderMode.FIRST_CLASS && fMode != Folder.FolderClass.FIRST_CLASS) + || (aMode == FolderMode.FIRST_AND_SECOND_CLASS && + fMode != Folder.FolderClass.FIRST_CLASS && + fMode != Folder.FolderClass.SECOND_CLASS) + || (aMode == FolderMode.NOT_SECOND_CLASS && fMode == Folder.FolderClass.SECOND_CLASS)) { + continue; } FolderInfoHolder holder = null; diff --git a/src/com/fsck/k9/activity/InsertableHtmlContent.java b/src/com/fsck/k9/activity/InsertableHtmlContent.java index 9821ad784..851f0ca9a 100644 --- a/src/com/fsck/k9/activity/InsertableHtmlContent.java +++ b/src/com/fsck/k9/activity/InsertableHtmlContent.java @@ -12,7 +12,7 @@ import java.io.Serializable; * * TODO: This container should also have a text part, along with its insertion point. Or maybe a generic InsertableContent and maintain one each for Html and Text? */ -public class InsertableHtmlContent implements Serializable { +class InsertableHtmlContent implements Serializable { private static final long serialVersionUID = 2397327034L; // Default to a headerInsertionPoint at the beginning of the message. private int headerInsertionPoint = 0; diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 141399bcc..ff8cbc28d 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -89,6 +89,7 @@ import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.ContactItem; import com.fsck.k9.helper.Contacts; +import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.IdentityHelper; import com.fsck.k9.helper.Utility; @@ -100,16 +101,17 @@ import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; 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.TextBodyBuilder; -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.mailstore.LocalAttachmentBody; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.TempFileBody; +import com.fsck.k9.mailstore.TempFileMessageBody; import com.fsck.k9.view.MessageWebView; import org.apache.james.mime4j.codec.EncoderUtil; @@ -435,17 +437,15 @@ public class MessageCompose extends K9Activity implements OnClickListener, * Get intent for composing a new message as a reply to the given message. If replyAll is true * the function is reply all instead of simply reply. * @param context - * @param account * @param message * @param replyAll * @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message */ public static Intent getActionReplyIntent( - Context context, - Account account, - Message message, - boolean replyAll, - String messageBody) { + Context context, + LocalMessage message, + boolean replyAll, + String messageBody) { Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_MESSAGE_BODY, messageBody); i.putExtra(EXTRA_MESSAGE_REFERENCE, message.makeMessageReference()); @@ -468,25 +468,22 @@ public class MessageCompose extends K9Activity implements OnClickListener, */ public static void actionReply( Context context, - Account account, - Message message, + LocalMessage message, boolean replyAll, String messageBody) { - context.startActivity(getActionReplyIntent(context, account, message, replyAll, messageBody)); + context.startActivity(getActionReplyIntent(context, message, replyAll, messageBody)); } /** * Compose a new message as a forward of the given message. * @param context - * @param account * @param message * @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message */ public static void actionForward( - Context context, - Account account, - Message message, - String messageBody) { + Context context, + LocalMessage message, + String messageBody) { Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_MESSAGE_BODY, messageBody); i.putExtra(EXTRA_MESSAGE_REFERENCE, message.makeMessageReference()); @@ -1339,7 +1336,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, */ private MimeMessage createMessage(boolean isDraft) throws MessagingException { MimeMessage message = new MimeMessage(); - message.addSentDate(new Date()); + message.addSentDate(new Date(), K9.hideTimeZone()); Address from = new Address(mIdentity.getEmail(), mIdentity.getName()); message.setFrom(from); message.setRecipients(RecipientType.TO, getAddresses(mToView)); @@ -1658,7 +1655,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, // First item is the body length. We use this to separate the composed reply from the quoted text. if (tokenizer.hasMoreTokens()) { - String bodyLengthS = Utility.base64Decode(tokenizer.nextToken()); + String bodyLengthS = Base64.decode(tokenizer.nextToken()); try { identity.put(IdentityField.LENGTH, Integer.valueOf(bodyLengthS).toString()); } catch (Exception e) { @@ -1666,16 +1663,16 @@ public class MessageCompose extends K9Activity implements OnClickListener, } } if (tokenizer.hasMoreTokens()) { - identity.put(IdentityField.SIGNATURE, Utility.base64Decode(tokenizer.nextToken())); + identity.put(IdentityField.SIGNATURE, Base64.decode(tokenizer.nextToken())); } if (tokenizer.hasMoreTokens()) { - identity.put(IdentityField.NAME, Utility.base64Decode(tokenizer.nextToken())); + identity.put(IdentityField.NAME, Base64.decode(tokenizer.nextToken())); } if (tokenizer.hasMoreTokens()) { - identity.put(IdentityField.EMAIL, Utility.base64Decode(tokenizer.nextToken())); + identity.put(IdentityField.EMAIL, Base64.decode(tokenizer.nextToken())); } if (tokenizer.hasMoreTokens()) { - identity.put(IdentityField.QUOTED_TEXT_MODE, Utility.base64Decode(tokenizer.nextToken())); + identity.put(IdentityField.QUOTED_TEXT_MODE, Base64.decode(tokenizer.nextToken())); } } @@ -2646,7 +2643,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, * @param message * The source message used to populate the various text fields. */ - private void processSourceMessage(Message message) { + private void processSourceMessage(LocalMessage message) { try { switch (mAction) { case REPLY: @@ -2800,7 +2797,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, } } - private void processDraftMessage(Message message) throws MessagingException { + private void processDraftMessage(LocalMessage message) throws MessagingException { String showQuotedTextMode = "NONE"; mDraftId = MessagingController.getInstance(getApplication()).getId(message); @@ -2852,7 +2849,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, newIdentity.setSignature(k9identity.get(IdentityField.SIGNATURE)); mSignatureChanged = true; } else { - newIdentity.setSignatureUse(message.getFolder().getAccount().getSignatureUse()); + newIdentity.setSignatureUse(message.getFolder().getSignatureUse()); newIdentity.setSignature(mIdentity.getSignature()); } @@ -2962,7 +2959,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, Part part = MimeUtility.findFirstPartByMimeType(message, "text/html"); if (part != null) { // Shouldn't happen if we were the one who saved it. mQuotedTextFormat = SimpleMessageFormat.HTML; - String text = MimeUtility.getTextFromPart(part); + String text = MessageExtractor.getTextFromPart(part); if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Loading message with offset " + bodyOffset + ", length " + bodyLength + ". Text length is " + text.length() + "."); } @@ -3027,7 +3024,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, boolean viewMessageContent) throws MessagingException { Part textPart = MimeUtility.findFirstPartByMimeType(message, "text/plain"); if (textPart != null) { - String text = MimeUtility.getTextFromPart(textPart); + String text = MessageExtractor.getTextFromPart(textPart); if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Loading message with offset " + bodyOffset + ", length " + bodyLength + ". Text length is " + text.length() + "."); } @@ -3230,7 +3227,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, HTML found."); } - return MimeUtility.getTextFromPart(part); + return MessageExtractor.getTextFromPart(part); } part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); @@ -3238,7 +3235,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found."); } - return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part)); + String text = MessageExtractor.getTextFromPart(part); + return HtmlConverter.textToHtml(text); } } else if (format == SimpleMessageFormat.TEXT) { // Text takes precedence, then html. @@ -3247,7 +3245,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, text found."); } - return MimeUtility.getTextFromPart(part); + return MessageExtractor.getTextFromPart(part); } part = MimeUtility.findFirstPartByMimeType(message, "text/html"); @@ -3255,7 +3253,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, HTML found."); } - return HtmlConverter.htmlToText(MimeUtility.getTextFromPart(part)); + String text = MessageExtractor.getTextFromPart(part); + return HtmlConverter.htmlToText(text); } } @@ -3437,7 +3436,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, } updateMessageFormat(); } else { - processSourceMessage(message); + processSourceMessage((LocalMessage) message); mSourceProcessed = true; } } diff --git a/src/com/fsck/k9/activity/MessageInfoHolder.java b/src/com/fsck/k9/activity/MessageInfoHolder.java index eecf4f80d..09708fbe6 100644 --- a/src/com/fsck/k9/activity/MessageInfoHolder.java +++ b/src/com/fsck/k9/activity/MessageInfoHolder.java @@ -1,7 +1,8 @@ package com.fsck.k9.activity; import java.util.Date; -import com.fsck.k9.mail.Message; + +import com.fsck.k9.mailstore.LocalMessage; public class MessageInfoHolder { public String date; @@ -18,7 +19,7 @@ public class MessageInfoHolder { public boolean forwarded; public boolean flagged; public boolean dirty; - public Message message; + public LocalMessage message; public FolderInfoHolder folder; public boolean selected; public String account; @@ -31,7 +32,7 @@ public class MessageInfoHolder { @Override public boolean equals(Object o) { - if (o instanceof MessageInfoHolder == false) { + if (!(o instanceof MessageInfoHolder)) { return false; } MessageInfoHolder other = (MessageInfoHolder)o; diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 7f079db3e..68f2c12ef 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -42,8 +42,8 @@ import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener; import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.fragment.MessageViewFragment.MessageViewFragmentListener; -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mailstore.StorageManager; +import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchAccount; import com.fsck.k9.search.SearchSpecification; @@ -1193,23 +1193,23 @@ public class MessageList extends K9Activity implements MessageListFragmentListen } @Override - public void onResendMessage(Message message) { + public void onResendMessage(LocalMessage message) { MessageCompose.actionEditDraft(this, message.makeMessageReference()); } @Override - public void onForward(Message message) { - MessageCompose.actionForward(this, message.getFolder().getAccount(), message, null); + public void onForward(LocalMessage message) { + MessageCompose.actionForward(this, message, null); } @Override - public void onReply(Message message) { - MessageCompose.actionReply(this, message.getFolder().getAccount(), message, false, null); + public void onReply(LocalMessage message) { + MessageCompose.actionReply(this, message, false, null); } @Override - public void onReplyAll(Message message) { - MessageCompose.actionReply(this, message.getFolder().getAccount(), message, true, null); + public void onReplyAll(LocalMessage message) { + MessageCompose.actionReply(this, message, true, null); } @Override @@ -1399,18 +1399,18 @@ public class MessageList extends K9Activity implements MessageListFragmentListen } @Override - public void onReply(Message message, PgpData pgpData) { - MessageCompose.actionReply(this, mAccount, message, false, pgpData.getDecryptedData()); + public void onReply(LocalMessage message, PgpData pgpData) { + MessageCompose.actionReply(this, message, false, pgpData.getDecryptedData()); } @Override - public void onReplyAll(Message message, PgpData pgpData) { - MessageCompose.actionReply(this, mAccount, message, true, pgpData.getDecryptedData()); + public void onReplyAll(LocalMessage message, PgpData pgpData) { + MessageCompose.actionReply(this, message, true, pgpData.getDecryptedData()); } @Override - public void onForward(Message mMessage, PgpData mPgpData) { - MessageCompose.actionForward(this, mAccount, mMessage, mPgpData.getDecryptedData()); + public void onForward(LocalMessage mMessage, PgpData mPgpData) { + MessageCompose.actionForward(this, mMessage, mPgpData.getDecryptedData()); } @Override diff --git a/src/com/fsck/k9/activity/MessageReference.java b/src/com/fsck/k9/activity/MessageReference.java index 990d99774..131b0715a 100644 --- a/src/com/fsck/k9/activity/MessageReference.java +++ b/src/com/fsck/k9/activity/MessageReference.java @@ -8,11 +8,11 @@ import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; -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.mailstore.LocalFolder; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mail.filter.Base64; import java.util.StringTokenizer; @@ -49,9 +49,9 @@ public class MessageReference implements Parcelable { // Split the identity, stripping away the first two characters representing the version and delimiter. StringTokenizer tokens = new StringTokenizer(identity.substring(2), IDENTITY_SEPARATOR, false); if (tokens.countTokens() >= 3) { - accountUuid = Utility.base64Decode(tokens.nextToken()); - folderName = Utility.base64Decode(tokens.nextToken()); - uid = Utility.base64Decode(tokens.nextToken()); + accountUuid = Base64.decode(tokens.nextToken()); + folderName = Base64.decode(tokens.nextToken()); + uid = Base64.decode(tokens.nextToken()); if (tokens.hasMoreTokens()) { final String flagString = tokens.nextToken(); @@ -80,11 +80,11 @@ public class MessageReference implements Parcelable { refString.append(IDENTITY_VERSION_1); refString.append(IDENTITY_SEPARATOR); - refString.append(Utility.base64Encode(accountUuid)); + refString.append(Base64.encode(accountUuid)); refString.append(IDENTITY_SEPARATOR); - refString.append(Utility.base64Encode(folderName)); + refString.append(Base64.encode(folderName)); refString.append(IDENTITY_SEPARATOR); - refString.append(Utility.base64Encode(uid)); + refString.append(Base64.encode(uid)); if (flag != null) { refString.append(IDENTITY_SEPARATOR); refString.append(flag.name()); @@ -128,13 +128,13 @@ public class MessageReference implements Parcelable { '}'; } - public Message restoreToLocalMessage(Context context) { + public LocalMessage restoreToLocalMessage(Context context) { try { Account account = Preferences.getPreferences(context).getAccount(accountUuid); if (account != null) { - Folder folder = account.getLocalStore().getFolder(folderName); + LocalFolder folder = account.getLocalStore().getFolder(folderName); if (folder != null) { - Message message = folder.getMessage(uid); + LocalMessage message = folder.getMessage(uid); if (message != null) { return message; } else { diff --git a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java b/src/com/fsck/k9/activity/TextBodyBuilder.java similarity index 97% rename from src/com/fsck/k9/mail/internet/TextBodyBuilder.java rename to src/com/fsck/k9/activity/TextBodyBuilder.java index 7efbc8f5d..4dc0a0666 100644 --- a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java +++ b/src/com/fsck/k9/activity/TextBodyBuilder.java @@ -1,14 +1,14 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.activity; import android.text.TextUtils; import android.util.Log; import com.fsck.k9.K9; -import com.fsck.k9.activity.InsertableHtmlContent; -import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.Body; +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.internet.TextBody; -public class TextBodyBuilder { +class TextBodyBuilder { private boolean mIncludeQuotedText = true; private boolean mReplyAfterQuote = false; private boolean mSignatureBeforeQuotedText = false; @@ -27,7 +27,7 @@ public class TextBodyBuilder { /** * Build the {@link Body} that will contain the text of the message. * - * @return {@link TextBody} instance that contains the entered text and + * @return {@link com.fsck.k9.mail.internet.TextBody} instance that contains the entered text and * possibly the quoted original message. */ public TextBody buildTextHtml() { diff --git a/src/com/fsck/k9/activity/UpgradeDatabases.java b/src/com/fsck/k9/activity/UpgradeDatabases.java index b0cf3587a..ae8cd6fd7 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.local.LocalStore#DB_VERSION}. This + * {@link SharedPreferences} file to {@link com.fsck.k9.mailstore.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 ba4a82658..c6ee715de 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -36,8 +36,8 @@ import com.fsck.k9.activity.K9PreferenceActivity; import com.fsck.k9.activity.ManageIdentities; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.StorageManager; import com.fsck.k9.service.MailService; import org.openintents.openpgp.util.OpenPgpListPreference; diff --git a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java index 56d409743..eab3db5fd 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java @@ -38,9 +38,9 @@ import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.store.ImapStore; +import com.fsck.k9.mail.store.RemoteStore; import com.fsck.k9.mail.transport.SmtpTransport; import com.fsck.k9.view.ClientCertificateSpinner; import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener; @@ -421,7 +421,7 @@ public class AccountSetupBasics extends K9Activity ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias); ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1, ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias); - String storeUri = Store.createStoreUri(storeServer); + String storeUri = RemoteStore.createStoreUri(storeServer); String transportUri = Transport.createTransportUri(transportServer); mAccount.setStoreUri(storeUri); mAccount.setTransportUri(transportUri); diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java index ff4cd531b..5d417309f 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -27,6 +27,7 @@ import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.store.ImapStore; import com.fsck.k9.mail.store.Pop3Store; +import com.fsck.k9.mail.store.RemoteStore; import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings; import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings; @@ -163,7 +164,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener } try { - ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri()); + ServerSettings settings = RemoteStore.decodeStoreUri(mAccount.getStoreUri()); if (savedInstanceState == null) { // The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter @@ -610,7 +611,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener ServerSettings settings = new ServerSettings(mStoreType, host, port, connectionSecurity, authType, username, password, clientCertificateAlias, extra); - mAccount.setStoreUri(Store.createStoreUri(settings)); + mAccount.setStoreUri(RemoteStore.createStoreUri(settings)); mAccount.setCompression(Account.TYPE_MOBILE, mCompressionMobile.isChecked()); mAccount.setCompression(Account.TYPE_WIFI, mCompressionWifi.isChecked()); diff --git a/src/com/fsck/k9/activity/setup/FolderSettings.java b/src/com/fsck/k9/activity/setup/FolderSettings.java index 31bb8e95c..737e2ffc3 100644 --- a/src/com/fsck/k9/activity/setup/FolderSettings.java +++ b/src/com/fsck/k9/activity/setup/FolderSettings.java @@ -16,8 +16,8 @@ import com.fsck.k9.mail.Folder.FolderClass; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.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 9b3e7d5e0..81b785997 100644 --- a/src/com/fsck/k9/cache/EmailProviderCache.java +++ b/src/com/fsck/k9/cache/EmailProviderCache.java @@ -11,8 +11,8 @@ import android.support.v4.content.LocalBroadcastManager; import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.local.LocalMessage; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.provider.EmailProvider; /** @@ -123,13 +123,11 @@ public class EmailProviderCache { } } - public void hideMessages(List messages) { + public void hideMessages(List messages) { synchronized (mHiddenMessageCache) { - for (Message message : messages) { - LocalMessage localMessage = (LocalMessage) message; - long messageId = localMessage.getId(); - long folderId = ((LocalFolder) localMessage.getFolder()).getId(); - mHiddenMessageCache.put(messageId, folderId); + for (LocalMessage message : messages) { + long messageId = message.getId(); + mHiddenMessageCache.put(messageId, message.getFolder().getId()); } } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 7f889650b..770742256 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -61,6 +61,7 @@ import com.fsck.k9.activity.setup.AccountSetupIncoming; import com.fsck.k9.activity.setup.AccountSetupOutgoing; import com.fsck.k9.cache.EmailProviderCache; import com.fsck.k9.helper.Contacts; +import com.fsck.k9.helper.MessageHelper; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.Address; @@ -78,16 +79,18 @@ import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.local.LocalMessage; -import com.fsck.k9.mail.store.local.LocalStore; -import com.fsck.k9.mail.store.local.LocalStore.PendingCommand; +import com.fsck.k9.mailstore.MessageRemovalListener; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.LocalStore; +import com.fsck.k9.mailstore.LocalStore.PendingCommand; import com.fsck.k9.mail.store.Pop3Store; -import com.fsck.k9.mail.store.UnavailableAccountException; -import com.fsck.k9.mail.store.UnavailableStorageException; +import com.fsck.k9.mailstore.UnavailableStorageException; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.StatsColumns; import com.fsck.k9.search.ConditionsTreeNode; @@ -212,7 +215,7 @@ public class MessagingController implements Runnable { * Don't modify this list directly, but use {@link addMessage} and * {@link removeMatchingMessage} instead. */ - LinkedList messages; + LinkedList messages; /** * List of references for messages that the user is still to be notified of, * but which don't fit into the inbox style anymore. It's sorted from newest @@ -236,7 +239,7 @@ public class MessagingController implements Runnable { public NotificationData(int unread) { unreadBeforeNotification = unread; droppedMessages = new LinkedList(); - messages = new LinkedList(); + messages = new LinkedList(); } /** @@ -247,9 +250,9 @@ public class MessagingController implements Runnable { * * @param m The new message to add. */ - public void addMessage(Message m) { + public void addMessage(LocalMessage m) { while (messages.size() >= MAX_MESSAGES) { - Message dropped = messages.removeLast(); + LocalMessage dropped = messages.removeLast(); droppedMessages.addFirst(dropped.makeMessageReference()); } messages.addFirst(m); @@ -270,10 +273,10 @@ public class MessagingController implements Runnable { } } - for (Message message : messages) { + for (LocalMessage message : messages) { if (message.makeMessageReference().equals(ref)) { if (messages.remove(message) && !droppedMessages.isEmpty()) { - Message restoredMessage = droppedMessages.getFirst().restoreToLocalMessage(context); + LocalMessage restoredMessage = droppedMessages.getFirst().restoreToLocalMessage(context); if (restoredMessage != null) { messages.addLast(restoredMessage); droppedMessages.removeFirst(); @@ -291,7 +294,7 @@ public class MessagingController implements Runnable { * List. */ public void supplyAllMessageRefs(List refs) { - for (Message m : messages) { + for (LocalMessage m : messages) { refs.add(m.makeMessageReference()); } refs.addAll(droppedMessages); @@ -312,7 +315,7 @@ public class MessagingController implements Runnable { private static final Set SYNC_FLAGS = EnumSet.of(Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED); - private void suppressMessages(Account account, List messages) { + private void suppressMessages(Account account, List messages) { EmailProviderCache cache = EmailProviderCache.getCache(account.getUuid(), mApplication.getApplicationContext()); cache.hideMessages(messages); @@ -324,13 +327,11 @@ public class MessagingController implements Runnable { cache.unhideMessages(messages); } - private boolean isMessageSuppressed(Account account, Message message) { - LocalMessage localMessage = (LocalMessage) message; - String accountUuid = account.getUuid(); - long messageId = localMessage.getId(); - long folderId = ((LocalFolder) localMessage.getFolder()).getId(); + private boolean isMessageSuppressed(LocalMessage message) { + long messageId = message.getId(); + long folderId = message.getFolder().getId(); - EmailProviderCache cache = EmailProviderCache.getCache(accountUuid, + EmailProviderCache cache = EmailProviderCache.getCache(message.getFolder().getAccountUuid(), mApplication.getApplicationContext()); return cache.isMessageHidden(messageId, folderId); } @@ -690,15 +691,16 @@ public class MessagingController implements Runnable { } // Collecting statistics of the search result - MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { + MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { @Override public void messageStarted(String message, int number, int ofTotal) {} @Override public void messagesFinished(int number) {} @Override - public void messageFinished(Message message, int number, int ofTotal) { - if (!isMessageSuppressed(message.getFolder().getAccount(), message)) { - List messages = new ArrayList(); + + public void messageFinished(LocalMessage message, int number, int ofTotal) { + if (!isMessageSuppressed(message)) { + List messages = new ArrayList(); messages.add(message); stats.unreadMessageCount += (!message.isSet(Flag.SEEN)) ? 1 : 0; @@ -762,7 +764,7 @@ public class MessagingController implements Runnable { final Account acct = Preferences.getPreferences(mApplication.getApplicationContext()).getAccount(acctUuid); if (listener != null) { - listener.remoteSearchStarted(acct, folderName); + listener.remoteSearchStarted(folderName); } List extraResults = new ArrayList(); @@ -791,7 +793,7 @@ public class MessagingController implements Runnable { messages.clear(); if (listener != null) { - listener.remoteSearchServerQueryComplete(acct, folderName, remoteMessages.size()); + listener.remoteSearchServerQueryComplete(folderName, remoteMessages.size(), acct.getRemoteSearchNumResults()); } Collections.sort(remoteMessages, new UidReverseComparator()); @@ -811,13 +813,13 @@ public class MessagingController implements Runnable { } else { Log.e(K9.LOG_TAG, "Could not complete remote search", e); if (listener != null) { - listener.remoteSearchFailed(acct, null, e.getMessage()); + listener.remoteSearchFailed(null, e.getMessage()); } addErrorMessage(acct, null, e); } } finally { if (listener != null) { - listener.remoteSearchFinished(acct, folderName, 0, extraResults); + listener.remoteSearchFinished(folderName, 0, acct.getRemoteSearchNumResults(), extraResults); } } @@ -878,7 +880,7 @@ public class MessagingController implements Runnable { } if (listener != null) { - listener.remoteSearchAddMessage(remoteFolder.getAccount(), remoteFolder.getName(), localMsg, i, messages.size()); + listener.remoteSearchAddMessage(remoteFolder.getName(), localMsg, i, messages.size()); } } } @@ -1455,7 +1457,7 @@ public class MessagingController implements Runnable { } } - private void fetchUnsyncedMessages(final Account account, final Folder remoteFolder, + private void fetchUnsyncedMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, List unsyncedMessages, final List smallMessages, @@ -1473,9 +1475,9 @@ public class MessagingController implements Runnable { final List chunk = new ArrayList(UNSYNC_CHUNK_SIZE); remoteFolder.fetch(unsyncedMessages, fp, - new MessageRetrievalListener() { + new MessageRetrievalListener() { @Override - public void messageFinished(Message message, int number, int ofTotal) { + public void messageFinished(T message, int number, int ofTotal) { try { String newPushState = remoteFolder.getNewPushState(localFolder.getPushState(), message); if (newPushState != null) { @@ -1564,7 +1566,7 @@ public class MessagingController implements Runnable { localFolder.appendMessages(messages); for (final Message message : messages) { - final Message localMessage = localFolder.getMessage(message.getUid()); + final LocalMessage localMessage = localFolder.getMessage(message.getUid()); syncFlags(localMessage, message); if (K9.DEBUG) Log.v(K9.LOG_TAG, "About to notify listeners that we got a new unsynced message " @@ -1592,9 +1594,9 @@ public class MessagingController implements Runnable { return true; } - private void downloadSmallMessages(final Account account, final Folder remoteFolder, + private void downloadSmallMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, - List smallMessages, + List smallMessages, final AtomicInteger progress, final int unreadBeforeStart, final AtomicInteger newMessages, @@ -1608,9 +1610,9 @@ public class MessagingController implements Runnable { Log.d(K9.LOG_TAG, "SYNC: Fetching small messages for folder " + folder); remoteFolder.fetch(smallMessages, - fp, new MessageRetrievalListener() { + fp, new MessageRetrievalListener() { @Override - public void messageFinished(final Message message, int number, int ofTotal) { + public void messageFinished(final T message, int number, int ofTotal) { try { if (!shouldImportMessage(account, folder, message, progress, earliestDate)) { @@ -1620,7 +1622,7 @@ public class MessagingController implements Runnable { } // Store the updated message locally - final Message localMessage = localFolder.storeSmallMessage(message, new Runnable() { + final LocalMessage localMessage = localFolder.storeSmallMessage(message, new Runnable() { @Override public void run() { progress.incrementAndGet(); @@ -1671,9 +1673,9 @@ public class MessagingController implements Runnable { - private void downloadLargeMessages(final Account account, final Folder remoteFolder, + private void downloadLargeMessages(final Account account, final Folder remoteFolder, final LocalFolder localFolder, - List largeMessages, + List largeMessages, final AtomicInteger progress, final int unreadBeforeStart, final AtomicInteger newMessages, @@ -1744,7 +1746,7 @@ public class MessagingController implements Runnable { * right now, attachments will be left for later. */ - Set viewables = MimeUtility.collectTextParts(message); + Set viewables = MessageExtractor.collectTextParts(message); /* * Now download the parts we're interested in storing. @@ -1768,7 +1770,7 @@ public class MessagingController implements Runnable { // Update the listener with what we've found progress.incrementAndGet(); // TODO do we need to re-fetch this here? - Message localMessage = localFolder.getMessage(message.getUid()); + LocalMessage localMessage = localFolder.getMessage(message.getUid()); // Increment the number of "new messages" if the newly downloaded message is // not marked as read. @@ -1821,11 +1823,11 @@ public class MessagingController implements Runnable { remoteFolder.fetch(undeletedMessages, fp, null); for (Message remoteMessage : syncFlagMessages) { - Message localMessage = localFolder.getMessage(remoteMessage.getUid()); + LocalMessage localMessage = localFolder.getMessage(remoteMessage.getUid()); boolean messageChanged = syncFlags(localMessage, remoteMessage); if (messageChanged) { boolean shouldBeNotifiedOf = false; - if (localMessage.isSet(Flag.DELETED) || isMessageSuppressed(account, localMessage)) { + if (localMessage.isSet(Flag.DELETED) || isMessageSuppressed(localMessage)) { for (MessagingListener l : getListeners()) { l.synchronizeMailboxRemovedMessage(account, folder, localMessage); } @@ -1859,13 +1861,13 @@ public class MessagingController implements Runnable { } } - private boolean syncFlags(Message localMessage, Message remoteMessage) throws MessagingException { + private boolean syncFlags(LocalMessage localMessage, Message remoteMessage) throws MessagingException { boolean messageChanged = false; if (localMessage == null || localMessage.isSet(Flag.DELETED)) { return false; } if (remoteMessage.isSet(Flag.DELETED)) { - if (localMessage.getFolder().getAccount().syncRemoteDeletions()) { + if (localMessage.getFolder().syncRemoteDeletions()) { localMessage.setFlag(Flag.DELETED, true); messageChanged = true; } @@ -2722,7 +2724,7 @@ public class MessagingController implements Runnable { long nowTime = System.currentTimeMillis(); Date nowDate = new Date(nowTime); message.setInternalDate(nowDate); - message.addSentDate(nowDate); + message.addSentDate(nowDate, K9.hideTimeZone()); message.setFrom(new Address(account.getEmail(), "K9mail internal")); localFolder.appendMessages(Collections.singletonList(message)); @@ -2857,7 +2859,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, List 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. @@ -3197,7 +3199,7 @@ public class MessagingController implements Runnable { try { LocalStore localStore = account.getLocalStore(); - List attachments = MimeUtility.collectAttachments(message); + List attachments = MessageExtractor.collectAttachments(message); for (Part attachment : attachments) { attachment.setBody(null); } @@ -3775,7 +3777,7 @@ public class MessagingController implements Runnable { } } public void moveMessages(final Account account, final String srcFolder, - final List messages, final String destFolder, + final List messages, final String destFolder, final MessagingListener listener) { suppressMessages(account, messages); @@ -3790,7 +3792,7 @@ public class MessagingController implements Runnable { } public void moveMessagesInThread(final Account account, final String srcFolder, - final List messages, final String destFolder) { + final List messages, final String destFolder) { suppressMessages(account, messages); @@ -3808,14 +3810,14 @@ public class MessagingController implements Runnable { }); } - public void moveMessage(final Account account, final String srcFolder, final Message message, + public void moveMessage(final Account account, final String srcFolder, final LocalMessage message, final String destFolder, final MessagingListener listener) { moveMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener); } public void copyMessages(final Account account, final String srcFolder, - final List messages, final String destFolder, + final List messages, final String destFolder, final MessagingListener listener) { putBackground("copyMessages", null, new Runnable() { @@ -3828,7 +3830,7 @@ public class MessagingController implements Runnable { } public void copyMessagesInThread(final Account account, final String srcFolder, - final List messages, final String destFolder) { + final List messages, final String destFolder) { putBackground("copyMessagesInThread", null, new Runnable() { @Override @@ -3851,7 +3853,7 @@ public class MessagingController implements Runnable { } private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, - final List inMessages, final String destFolder, final boolean isCopy, + final List inMessages, final String destFolder, final boolean isCopy, MessagingListener listener) { try { @@ -3962,7 +3964,7 @@ public class MessagingController implements Runnable { localFolder.open(Folder.OPEN_MODE_RW); String uid = localFolder.getMessageUidById(id); if (uid != null) { - Message message = localFolder.getMessage(uid); + LocalMessage message = localFolder.getMessage(uid); if (message != null) { deleteMessages(Collections.singletonList(message), null); } @@ -3974,7 +3976,7 @@ public class MessagingController implements Runnable { } } - public void deleteThreads(final List messages) { + public void deleteThreads(final List messages) { actOnMessages(messages, new MessageActor() { @Override @@ -4006,7 +4008,7 @@ public class MessagingController implements Runnable { } } - public List collectMessagesInThreads(Account account, List messages) + public List collectMessagesInThreads(Account account, List messages) throws MessagingException { LocalStore localStore = account.getLocalStore(); @@ -4025,7 +4027,7 @@ public class MessagingController implements Runnable { return messagesInThreads; } - public void deleteMessages(final List messages, final MessagingListener listener) { + public void deleteMessages(final List messages, final MessagingListener listener) { actOnMessages(messages, new MessageActor() { @Override @@ -4244,13 +4246,12 @@ public class MessagingController implements Runnable { try { Intent msg = new Intent(Intent.ACTION_SEND); String quotedText = null; - Part part = MimeUtility.findFirstPartByMimeType(message, - "text/plain"); + Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); if (part == null) { part = MimeUtility.findFirstPartByMimeType(message, "text/html"); } if (part != null) { - quotedText = MimeUtility.getTextFromPart(part); + quotedText = MessageExtractor.getTextFromPart(part); } if (quotedText != null) { msg.putExtra(Intent.EXTRA_TEXT, quotedText); @@ -4393,7 +4394,6 @@ public class MessagingController implements Runnable { Store localStore = account.getLocalStore(); for (final Folder folder : localStore.getPersonalNamespaces(false)) { folder.open(Folder.OPEN_MODE_RW); - folder.refresh(prefs); Folder.FolderClass fDisplayClass = folder.getDisplayClass(); Folder.FolderClass fSyncClass = folder.getSyncClass(); @@ -4696,7 +4696,7 @@ public class MessagingController implements Runnable { if (fromAddrs != null) { isSelf = account.isAnIdentity(fromAddrs); if (!isSelf && fromAddrs.length > 0) { - return fromAddrs[0].toFriendly(contacts).toString(); + return MessageHelper.toFriendly(fromAddrs[0], contacts).toString(); } } @@ -4706,7 +4706,7 @@ public class MessagingController implements Runnable { if (rcpts != null && rcpts.length > 0) { return context.getString(R.string.message_to_fmt, - rcpts[0].toFriendly(contacts).toString()); + MessageHelper.toFriendly(rcpts[0], contacts).toString()); } return context.getString(R.string.general_no_sender); @@ -4781,8 +4781,7 @@ public class MessagingController implements Runnable { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } - private Message findNewestMessageForNotificationLocked(Context context, - Account account, NotificationData data) { + private LocalMessage findNewestMessageForNotificationLocked(Context context, NotificationData data) { if (!data.messages.isEmpty()) { return data.messages.getFirst(); } @@ -4798,7 +4797,7 @@ public class MessagingController implements Runnable { * Creates a notification of a newly received message. */ private void notifyAccount(Context context, Account account, - Message message, int previousUnreadMessageCount) { + LocalMessage message, int previousUnreadMessageCount) { final NotificationData data = getNotificationData(account, previousUnreadMessageCount); synchronized (data) { notifyAccountWithDataLocked(context, account, message, data); @@ -4809,12 +4808,12 @@ public class MessagingController implements Runnable { private static final int NUM_SENDERS_IN_LOCK_SCREEN_NOTIFICATION = 5; private void notifyAccountWithDataLocked(Context context, Account account, - Message message, NotificationData data) { + LocalMessage message, NotificationData data) { boolean updateSilently = false; if (message == null) { /* this can happen if a message we previously notified for is read or deleted remotely */ - message = findNewestMessageForNotificationLocked(context, account, data); + message = findNewestMessageForNotificationLocked(context, data); updateSilently = true; if (message == null) { // seemingly both the message list as well as the overflow list is empty; @@ -5110,7 +5109,7 @@ public class MessagingController implements Runnable { int unreadCount, CharSequence accountDescription, CharSequence formattedSender, - List messages) { + List messages) { if (!platformSupportsLockScreenNotifications()) { return; } @@ -5307,7 +5306,6 @@ public class MessagingController implements Runnable { continue; } folder.open(Folder.OPEN_MODE_RW); - folder.refresh(prefs); Folder.FolderClass fDisplayClass = folder.getDisplayClass(); Folder.FolderClass fPushClass = folder.getPushClass(); @@ -5685,15 +5683,15 @@ public class MessagingController implements Runnable { } - private void actOnMessages(List messages, MessageActor actor) { + private void actOnMessages(List messages, MessageActor actor) { Map>> accountMap = new HashMap>>(); - for (Message message : messages) { + for (LocalMessage message : messages) { if ( message == null) { continue; } Folder folder = message.getFolder(); - Account account = folder.getAccount(); + Account account = message.getAccount(); Map> folderMap = accountMap.get(account); if (folderMap == null) { diff --git a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java index ccd13a59f..3bdec8c22 100644 --- a/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java +++ b/src/com/fsck/k9/controller/MessagingControllerPushReceiver.java @@ -11,8 +11,8 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.PushReceiver; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.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 5c5a66f50..72bd93d18 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -11,6 +11,7 @@ import com.fsck.k9.BaseAccount; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; +import com.fsck.k9.mailstore.LocalMessage; /** * Defines the interface that {@link MessagingController} will use to callback to requesters. @@ -42,10 +43,8 @@ public class MessagingListener { public void listLocalMessagesStarted(Account account, String folder) {} - public void listLocalMessages(Account account, String folder, Message[] messages) {} - public void listLocalMessagesAddMessages(Account account, String folder, - List messages) {} + List messages) {} public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) {} @@ -156,10 +155,9 @@ public class MessagingListener { /** * Called when a remote search is started * - * @param acct * @param folder */ - public void remoteSearchStarted(Account acct, String folder) {} + public void remoteSearchStarted(String folder) {} /** @@ -167,35 +165,30 @@ public class MessagingListener { * * @param numResults */ - public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { } + public void remoteSearchServerQueryComplete(String folderName, int numResults, int maxResults) { } /** * Called when a new result message is available for a remote search * Can assume headers have been downloaded, but potentially not body. - * @param account * @param folder * @param message */ - public void remoteSearchAddMessage(Account account, String folder, Message message, int numDone, int numTotal) { } + public void remoteSearchAddMessage(String folder, Message message, int numDone, int numTotal) { } /** * Called when Remote Search is fully complete - * - * @param acct - * @param folder + * @param folder * @param numResults */ - public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) {} + public void remoteSearchFinished(String folder, int numResults, int maxResults, List extraResults) {} /** * Called when there was a problem with a remote search operation. - * - * @param acct - * @param folder + * @param folder * @param err */ - public void remoteSearchFailed(Account acct, String folder, String err) { } + public void remoteSearchFailed(String folder, String err) { } /** * General notification messages subclasses can override to be notified that the controller diff --git a/src/com/fsck/k9/mail/store/UnavailableAccountException.java b/src/com/fsck/k9/controller/UnavailableAccountException.java similarity index 83% rename from src/com/fsck/k9/mail/store/UnavailableAccountException.java rename to src/com/fsck/k9/controller/UnavailableAccountException.java index 26e66b1be..55533828e 100644 --- a/src/com/fsck/k9/mail/store/UnavailableAccountException.java +++ b/src/com/fsck/k9/controller/UnavailableAccountException.java @@ -1,10 +1,8 @@ -package com.fsck.k9.mail.store; - -import com.fsck.k9.Account; +package com.fsck.k9.controller; /** - * An {@link Account} is not - * {@link Account#isAvailable(android.content.Context)}.
    + * An {@link com.fsck.k9.Account} is not + * {@link com.fsck.k9.Account#isAvailable(android.content.Context)}.
    * The operation may be retried later. */ public class UnavailableAccountException extends RuntimeException { diff --git a/src/com/fsck/k9/crypto/CryptoHelper.java b/src/com/fsck/k9/crypto/CryptoHelper.java index 3ce2787c7..612c4ba3d 100644 --- a/src/com/fsck/k9/crypto/CryptoHelper.java +++ b/src/com/fsck/k9/crypto/CryptoHelper.java @@ -7,8 +7,10 @@ import java.util.regex.Pattern; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeUtility; + public class CryptoHelper { public static Pattern PGP_MESSAGE = @@ -37,7 +39,7 @@ public class CryptoHelper { part = MimeUtility.findFirstPartByMimeType(message, "text/html"); } if (part != null) { - data = MimeUtility.getTextFromPart(part); + data = MessageExtractor.getTextFromPart(part); } } catch (MessagingException e) { // guess not... @@ -60,7 +62,7 @@ public class CryptoHelper { part = MimeUtility.findFirstPartByMimeType(message, "text/html"); } if (part != null) { - data = MimeUtility.getTextFromPart(part); + data = MessageExtractor.getTextFromPart(part); } } catch (MessagingException e) { // guess not... diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 80b19b33a..c14c4fc96 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -88,8 +88,9 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.store.local.LocalFolder; -import com.fsck.k9.mail.store.local.LocalStore; +import com.fsck.k9.mailstore.LocalFolder; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.provider.EmailProvider.SpecialColumns; @@ -427,7 +428,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * Relevant messages for the current context when we have to remember the chosen messages * between user interactions (e.g. selecting a folder for move operation). */ - private List mActiveMessages; + private List mActiveMessages; /* package visibility for faster inner class access */ MessageHelper mMessageHelper; @@ -1035,7 +1036,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener return null; } - private Folder getFolderById(Account account, long folderId) { + private LocalFolder getFolderById(Account account, long folderId) { try { LocalStore localStore = account.getLocalStore(); LocalFolder localFolder = localStore.getFolderById(folderId); @@ -1189,19 +1190,19 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } } - public void onReply(Message message) { + public void onReply(LocalMessage message) { mFragmentListener.onReply(message); } - public void onReplyAll(Message message) { + public void onReplyAll(LocalMessage message) { mFragmentListener.onReplyAll(message); } - public void onForward(Message message) { + public void onForward(LocalMessage message) { mFragmentListener.onForward(message); } - public void onResendMessage(Message message) { + public void onResendMessage(LocalMessage message) { mFragmentListener.onResendMessage(message); } @@ -1309,11 +1310,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener changeSort(sorts[curIndex]); } - private void onDelete(Message message) { + private void onDelete(LocalMessage message) { onDelete(Collections.singletonList(message)); } - private void onDelete(List messages) { + private void onDelete(List messages) { if (K9.confirmDelete()) { // remember the message selection for #onCreateDialog(int) mActiveMessages = messages; @@ -1323,7 +1324,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } } - private void onDeleteConfirmed(List messages) { + private void onDeleteConfirmed(List messages) { if (mThreadedList) { mController.deleteThreads(messages); } else { @@ -1345,15 +1346,14 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); - final List messages = mActiveMessages; + final List messages = mActiveMessages; if (destFolderName != null) { mActiveMessages = null; // don't need it any more if (messages.size() > 0) { - Account account = messages.get(0).getFolder().getAccount(); - account.setLastSelectedFolderName(destFolderName); + messages.get(0).getFolder().setLastSelectedFolderName(destFolderName); } switch (requestCode) { @@ -1510,23 +1510,19 @@ public class MessageListFragment extends Fragment implements OnItemClickListener break; } case R.id.reply: { - Message message = getMessageAtPosition(adapterPosition); - onReply(message); + onReply(getMessageAtPosition(adapterPosition)); break; } case R.id.reply_all: { - Message message = getMessageAtPosition(adapterPosition); - onReplyAll(message); + onReplyAll(getMessageAtPosition(adapterPosition)); break; } case R.id.forward: { - Message message = getMessageAtPosition(adapterPosition); - onForward(message); + onForward(getMessageAtPosition(adapterPosition)); break; } case R.id.send_again: { - Message message = getMessageAtPosition(adapterPosition); - onResendMessage(message); + onResendMessage(getMessageAtPosition(adapterPosition)); mSelectedCount = 0; break; } @@ -1539,7 +1535,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener break; } case R.id.delete: { - Message message = getMessageAtPosition(adapterPosition); + LocalMessage message = getMessageAtPosition(adapterPosition); onDelete(message); break; } @@ -1562,23 +1558,19 @@ public class MessageListFragment extends Fragment implements OnItemClickListener // only if the account supports this case R.id.archive: { - Message message = getMessageAtPosition(adapterPosition); - onArchive(message); + onArchive(getMessageAtPosition(adapterPosition)); break; } case R.id.spam: { - Message message = getMessageAtPosition(adapterPosition); - onSpam(message); + onSpam(getMessageAtPosition(adapterPosition)); break; } case R.id.move: { - Message message = getMessageAtPosition(adapterPosition); - onMove(message); + onMove(getMessageAtPosition(adapterPosition)); break; } case R.id.copy: { - Message message = getMessageAtPosition(adapterPosition); - onCopy(message); + onCopy(getMessageAtPosition(adapterPosition)); break; } } @@ -1711,7 +1703,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener class MessageListActivityListener extends ActivityListener { @Override - public void remoteSearchFailed(Account acct, String folder, final String err) { + public void remoteSearchFailed(String folder, final String err) { mHandler.post(new Runnable() { @Override public void run() { @@ -1725,7 +1717,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } @Override - public void remoteSearchStarted(Account acct, String folder) { + public void remoteSearchStarted(String folder) { mHandler.progress(true); mHandler.updateFooter(mContext.getString(R.string.remote_search_sending_query)); } @@ -1736,12 +1728,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } @Override - public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) { + public void remoteSearchFinished(String folder, int numResults, int maxResults, List extraResults) { mHandler.progress(false); mHandler.remoteSearchFinished(); mExtraSearchResults = extraResults; if (extraResults != null && extraResults.size() > 0) { - mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults())); + mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), maxResults)); } else { mHandler.updateFooter(""); } @@ -1750,11 +1742,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } @Override - public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { + public void remoteSearchServerQueryComplete(String folderName, int numResults, int maxResults) { mHandler.progress(true); - if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) { + if (maxResults != 0 && numResults > maxResults) { mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading_limited, - account.getRemoteSearchNumResults(), numResults)); + maxResults, numResults)); } else { mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading, numResults)); } @@ -2416,7 +2408,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener computeBatchDirection(); } - private void onMove(Message message) { + private void onMove(LocalMessage message) { onMove(Collections.singletonList(message)); } @@ -2426,7 +2418,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param messages * Never {@code null}. */ - private void onMove(List messages) { + private void onMove(List messages) { if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) { return; } @@ -2440,12 +2432,13 @@ public class MessageListFragment extends Fragment implements OnItemClickListener folder = null; } - Account account = messages.get(0).getFolder().getAccount(); - displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, account, folder, messages); + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, + messages.get(0).getFolder().getAccountUuid(), null, + messages); } - private void onCopy(Message message) { + private void onCopy(LocalMessage message) { onCopy(Collections.singletonList(message)); } @@ -2455,7 +2448,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param messages * Never {@code null}. */ - private void onCopy(List messages) { + private void onCopy(List messages) { if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) { return; } @@ -2469,7 +2462,10 @@ public class MessageListFragment extends Fragment implements OnItemClickListener folder = null; } - displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, mAccount, folder, messages); + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, + messages.get(0).getFolder().getAccountUuid(), + null, + messages); } /** @@ -2486,12 +2482,13 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * * @see #startActivityForResult(Intent, int) */ - private void displayFolderChoice(int requestCode, Account account, Folder folder, - List messages) { + private void displayFolderChoice(int requestCode, Folder folder, + String accountUuid, String lastSelectedFolderName, + List messages) { Intent intent = new Intent(getActivity(), ChooseFolder.class); - intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, account.getUuid()); - intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, account.getLastSelectedFolderName()); + intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, accountUuid); + intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, lastSelectedFolderName); if (folder == null) { intent.putExtra(ChooseFolder.EXTRA_SHOW_CURRENT, "yes"); @@ -2504,14 +2501,14 @@ public class MessageListFragment extends Fragment implements OnItemClickListener startActivityForResult(intent, requestCode); } - private void onArchive(final Message message) { + private void onArchive(final LocalMessage message) { onArchive(Collections.singletonList(message)); } - private void onArchive(final List messages) { - Map> messagesByAccount = groupMessagesByAccount(messages); + private void onArchive(final List messages) { + Map> messagesByAccount = groupMessagesByAccount(messages); - for (Entry> entry : messagesByAccount.entrySet()) { + for (Entry> entry : messagesByAccount.entrySet()) { Account account = entry.getKey(); String archiveFolder = account.getArchiveFolderName(); @@ -2521,14 +2518,14 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } } - private Map> groupMessagesByAccount(final List messages) { - Map> messagesByAccount = new HashMap>(); - for (Message message : messages) { - Account account = message.getFolder().getAccount(); + private Map> groupMessagesByAccount(final List messages) { + Map> messagesByAccount = new HashMap>(); + for (LocalMessage message : messages) { + Account account = message.getAccount(); - List msgList = messagesByAccount.get(account); + List msgList = messagesByAccount.get(account); if (msgList == null) { - msgList = new ArrayList(); + msgList = new ArrayList(); messagesByAccount.put(account, msgList); } @@ -2537,7 +2534,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener return messagesByAccount; } - private void onSpam(Message message) { + private void onSpam(LocalMessage message) { onSpam(Collections.singletonList(message)); } @@ -2547,7 +2544,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param messages * The messages to move to the spam folder. Never {@code null}. */ - private void onSpam(List messages) { + private void onSpam(List messages) { if (K9.confirmSpam()) { // remember the message selection for #onCreateDialog(int) mActiveMessages = messages; @@ -2557,10 +2554,10 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } } - private void onSpamConfirmed(List messages) { - Map> messagesByAccount = groupMessagesByAccount(messages); + private void onSpamConfirmed(List messages) { + Map> messagesByAccount = groupMessagesByAccount(messages); - for (Entry> entry : messagesByAccount.entrySet()) { + for (Entry> entry : messagesByAccount.entrySet()) { Account account = entry.getKey(); String spamFolder = account.getSpamFolderName(); @@ -2584,7 +2581,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * * @return {@code true}, if operation is possible. */ - private boolean checkCopyOrMovePossible(final List messages, + private boolean checkCopyOrMovePossible(final List messages, final FolderOperation operation) { if (messages.isEmpty()) { @@ -2592,11 +2589,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } boolean first = true; - for (final Message message : messages) { + for (final LocalMessage message : messages) { if (first) { first = false; // account check - final Account account = message.getFolder().getAccount(); + final Account account = message.getAccount(); if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { return false; @@ -2622,7 +2619,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param destination * The name of the destination folder. Never {@code null}. */ - private void copy(List messages, final String destination) { + private void copy(List messages, final String destination) { copyOrMove(messages, destination, FolderOperation.COPY); } @@ -2634,7 +2631,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param destination * The name of the destination folder. Never {@code null}. */ - private void move(List messages, final String destination) { + private void move(List messages, final String destination) { copyOrMove(messages, destination, FolderOperation.MOVE); } @@ -2650,12 +2647,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener * @param operation * Specifies what operation to perform. Never {@code null}. */ - private void copyOrMove(List messages, final String destination, + private void copyOrMove(List messages, final String destination, final FolderOperation operation) { - Map> folderMap = new HashMap>(); + Map> folderMap = new HashMap>(); - for (Message message : messages) { + for (LocalMessage message : messages) { if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { @@ -2674,19 +2671,19 @@ public class MessageListFragment extends Fragment implements OnItemClickListener continue; } - List outMessages = folderMap.get(folderName); + List outMessages = folderMap.get(folderName); if (outMessages == null) { - outMessages = new ArrayList(); + outMessages = new ArrayList(); folderMap.put(folderName, outMessages); } outMessages.add(message); } - for (Map.Entry> entry : folderMap.entrySet()) { + for (Map.Entry> entry : folderMap.entrySet()) { String folderName = entry.getKey(); - List outMessages = entry.getValue(); - Account account = outMessages.get(0).getFolder().getAccount(); + List outMessages = entry.getValue(); + Account account = outMessages.get(0).getAccount(); if (operation == FolderOperation.MOVE) { if (mThreadedList) { @@ -2859,7 +2856,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener */ switch (item.getItemId()) { case R.id.delete: { - List messages = getCheckedMessages(); + List messages = getCheckedMessages(); onDelete(messages); mSelectedCount = 0; break; @@ -2887,26 +2884,22 @@ public class MessageListFragment extends Fragment implements OnItemClickListener // only if the account supports this case R.id.archive: { - List messages = getCheckedMessages(); - onArchive(messages); + onArchive(getCheckedMessages()); mSelectedCount = 0; break; } case R.id.spam: { - List messages = getCheckedMessages(); - onSpam(messages); + onSpam(getCheckedMessages()); mSelectedCount = 0; break; } case R.id.move: { - List messages = getCheckedMessages(); - onMove(messages); + onMove(getCheckedMessages()); mSelectedCount = 0; break; } case R.id.copy: { - List messages = getCheckedMessages(); - onCopy(messages); + onCopy(getCheckedMessages()); mSelectedCount = 0; break; } @@ -2987,7 +2980,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener final Folder remoteFolder = mCurrentFolder.folder; remoteFolder.close(); // Send a remoteSearchFinished() message for good measure. - mListener.remoteSearchFinished(searchAccount, mCurrentFolder.name, 0, null); + mListener.remoteSearchFinished(mCurrentFolder.name, 0, searchAccount.getRemoteSearchNumResults(), null); } catch (Exception e) { // Since the user is going back, log and squash any exceptions. Log.e(K9.LOG_TAG, "Could not abort remote search before going back", e); @@ -3116,10 +3109,10 @@ public class MessageListFragment extends Fragment implements OnItemClickListener void setMessageListProgress(int level); void showThread(Account account, String folderName, long rootId); void showMoreFromSameSender(String senderAddress); - void onResendMessage(Message message); - void onForward(Message message); - void onReply(Message message); - void onReplyAll(Message message); + void onResendMessage(LocalMessage message); + void onForward(LocalMessage message); + void onReply(LocalMessage message); + void onReplyAll(LocalMessage message); void openMessage(MessageReference messageReference); void setMessageListTitle(String title); void setMessageListSubTitle(String subTitle); @@ -3135,7 +3128,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener changeSort(mSortType); } - private Message getSelectedMessage() { + private LocalMessage getSelectedMessage() { int listViewPosition = mListView.getSelectedItemPosition(); int adapterPosition = listViewToAdapterPosition(listViewPosition); @@ -3158,7 +3151,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener return AdapterView.INVALID_POSITION; } - private Message getMessageAtPosition(int adapterPosition) { + private LocalMessage getMessageAtPosition(int adapterPosition) { if (adapterPosition == AdapterView.INVALID_POSITION) { return null; } @@ -3168,7 +3161,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener Account account = getAccountFromCursor(cursor); long folderId = cursor.getLong(FOLDER_ID_COLUMN); - Folder folder = getFolderById(account, folderId); + LocalFolder folder = getFolderById(account, folderId); try { return folder.getMessage(uid); @@ -3179,14 +3172,14 @@ public class MessageListFragment extends Fragment implements OnItemClickListener return null; } - private List getCheckedMessages() { - List messages = new ArrayList(mSelected.size()); + private List getCheckedMessages() { + List messages = new ArrayList(mSelected.size()); for (int position = 0, end = mAdapter.getCount(); position < end; position++) { Cursor cursor = (Cursor) mAdapter.getItem(position); long uniqueId = cursor.getLong(mUniqueIdColumn); if (mSelected.contains(uniqueId)) { - Message message = getMessageAtPosition(position); + LocalMessage message = getMessageAtPosition(position); if (message != null) { messages.add(message); } @@ -3197,7 +3190,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } public void onDelete() { - Message message = getSelectedMessage(); + LocalMessage message = getSelectedMessage(); if (message != null) { onDelete(Collections.singletonList(message)); } @@ -3227,21 +3220,21 @@ public class MessageListFragment extends Fragment implements OnItemClickListener } public void onMove() { - Message message = getSelectedMessage(); + LocalMessage message = getSelectedMessage(); if (message != null) { onMove(message); } } public void onArchive() { - Message message = getSelectedMessage(); + LocalMessage message = getSelectedMessage(); if (message != null) { onArchive(message); } } public void onCopy() { - Message message = getSelectedMessage(); + LocalMessage message = getSelectedMessage(); if (message != null) { onCopy(message); } diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index 3a8bb4ddd..961a38555 100644 --- a/src/com/fsck/k9/fragment/MessageViewFragment.java +++ b/src/com/fsck/k9/fragment/MessageViewFragment.java @@ -38,7 +38,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.store.local.LocalMessage; +import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; @@ -75,7 +75,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private PgpData mPgpData; private Account mAccount; private MessageReference mMessageReference; - private Message mMessage; + private LocalMessage mMessage; private MessagingController mController; private Listener mListener = new Listener(); private MessageViewHandler mHandler = new MessageViewHandler(); @@ -311,7 +311,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, // Disable the delete button after it's tapped (to try to prevent // accidental clicks) mFragmentListener.disableDeleteAction(); - Message messageToDelete = mMessage; + LocalMessage messageToDelete = mMessage; mFragmentListener.showNextMessageOrReturn(); mController.deleteMessages(Collections.singletonList(messageToDelete), null); } @@ -341,7 +341,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private void refileMessage(String dstFolder) { String srcFolder = mMessageReference.folderName; - Message messageToMove = mMessage; + LocalMessage messageToMove = mMessage; mFragmentListener.showNextMessageOrReturn(); mController.moveMessage(mAccount, srcFolder, messageToMove, dstFolder, null); } @@ -576,7 +576,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, @Override public void loadMessageForViewBodyAvailable(final Account account, String folder, String uid, final Message message) { - if (!mMessageReference.uid.equals(uid) || + if (!(message instanceof LocalMessage) || + !mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; @@ -586,7 +587,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, @Override public void run() { try { - mMessage = message; + mMessage = (LocalMessage) message; mMessageView.setMessage(account, (LocalMessage) message, mPgpData, mController, mListener); mFragmentListener.updateMenu(); @@ -845,10 +846,10 @@ public class MessageViewFragment extends Fragment implements OnClickListener, } public interface MessageViewFragmentListener { - public void onForward(Message mMessage, PgpData mPgpData); + public void onForward(LocalMessage mMessage, PgpData mPgpData); public void disableDeleteAction(); - public void onReplyAll(Message mMessage, PgpData mPgpData); - public void onReply(Message mMessage, PgpData mPgpData); + public void onReplyAll(LocalMessage mMessage, PgpData mPgpData); + public void onReply(LocalMessage mMessage, PgpData mPgpData); public void displayMessageSubject(String title); public void setProgress(boolean b); public void showNextMessageOrReturn(); diff --git a/src/com/fsck/k9/helper/HtmlConverter.java b/src/com/fsck/k9/helper/HtmlConverter.java index 14eb75fda..28bf875b4 100644 --- a/src/com/fsck/k9/helper/HtmlConverter.java +++ b/src/com/fsck/k9/helper/HtmlConverter.java @@ -4,6 +4,7 @@ import android.text.*; import android.text.Html.TagHandler; import android.util.Log; import com.fsck.k9.K9; + import org.xml.sax.XMLReader; import java.io.IOException; @@ -13,11 +14,75 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Contains common routines to convert html to text and vice versa. */ public class HtmlConverter { + /* This comprises most common used Unicode characters allowed in IRI + * as detailed in RFC 3987. + * Specifically, those two byte Unicode characters are not included. + */ + private static final String GOOD_IRI_CHAR = + "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + /** + * Goegular expression to match all IANA top-level domains for WEB_URL. + * List accurate as of 2011/01/12. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py + */ + private static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = + "(?:" + + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(?:biz|b[abdefghijmnorstvwyz])" + + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(?:edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(?:gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(?:info|int|i[delmnoqrst])" + + "|(?:jobs|j[emop])" + + "|k[eghimnprwyz]" + + "|l[abcikrstuvy]" + + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" + + "|(?:name|net|n[acefgilopruz])" + + "|(?:org|om)" + + "|(?:pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eosuw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agksyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah)" + + "|y[et]" + + "|z[amw]))"; + private static final String BITCOIN_URI_PATTERN = + "bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?"; + /** + * Regular expression pattern to match most part of RFC 3987 + * Internationalized URLs, aka IRIs. Commonly used Unicode characters are + * added. + */ + private static final Pattern WEB_URL_PATTERN = Pattern.compile( + "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + + "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host + + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL + + "|(?:(?:25[0-5]|2[0-4]" // or ip address + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" + + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9])))" + + "(?:\\:\\d{1,5})?)" // plus option port number + + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + + "(?:\\b|$)"); // and finally, a word boundary or end of + /** * When generating previews, Spannable objects that can't be converted into a String are * represented as 0xfffc. When displayed, these show up as undisplayed squares. These constants @@ -381,9 +446,9 @@ public class HtmlConverter { * @param outputBuffer Buffer to append linked text to. */ protected static void linkifyText(final String text, final StringBuffer outputBuffer) { - String prepared = text.replaceAll(Regex.BITCOIN_URI_PATTERN, "$0"); + String prepared = text.replaceAll(BITCOIN_URI_PATTERN, "$0"); - Matcher m = Regex.WEB_URL_PATTERN.matcher(prepared); + Matcher m = WEB_URL_PATTERN.matcher(prepared); while (m.find()) { int start = m.start(); if (start == 0 || (start != 0 && prepared.charAt(start - 1) != '@')) { diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java index 26430b9e8..ddc2f4a4d 100644 --- a/src/com/fsck/k9/helper/MessageHelper.java +++ b/src/com/fsck/k9/helper/MessageHelper.java @@ -1,7 +1,11 @@ package com.fsck.k9.helper; import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.Log; import com.fsck.k9.Account; @@ -11,11 +15,23 @@ import com.fsck.k9.activity.FolderInfoHolder; import com.fsck.k9.activity.MessageInfoHolder; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; -import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mailstore.LocalMessage; public class MessageHelper { + /** + * If the number of addresses exceeds this value the addresses aren't + * resolved to the names of Android contacts. + * + *

    + * TODO: This number was chosen arbitrarily and should be determined by + * performance tests. + *

    + * + * @see #toFriendly(Address[], com.fsck.k9.helper.Contacts) + */ + private static final int TOO_MANY_ADDRESSES = 50; private static MessageHelper sInstance; @@ -32,8 +48,10 @@ public class MessageHelper { mContext = context; } - public void populate(final MessageInfoHolder target, final Message message, - final FolderInfoHolder folder, final Account account) { + public void populate(final MessageInfoHolder target, + final LocalMessage message, + final FolderInfoHolder folder, + Account account) { final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null; try { target.message = message; @@ -53,11 +71,11 @@ public class MessageHelper { Address[] addrs = message.getFrom(); if (addrs.length > 0 && account.isAnIdentity(addrs[0])) { - CharSequence to = Address.toFriendly(message .getRecipients(RecipientType.TO), contactHelper); + CharSequence to = toFriendly(message.getRecipients(RecipientType.TO), contactHelper); target.compareCounterparty = to.toString(); target.sender = new SpannableStringBuilder(mContext.getString(R.string.message_to_label)).append(to); } else { - target.sender = Address.toFriendly(addrs, contactHelper); + target.sender = toFriendly(addrs, contactHelper); target.compareCounterparty = target.sender.toString(); } @@ -68,14 +86,9 @@ public class MessageHelper { target.senderAddress = target.compareCounterparty; } - - - target.uid = message.getUid(); - - target.account = account.getUuid(); - target.uri = "email://messages/" + account.getAccountNumber() + "/" + message.getFolder().getName() + "/" + message.getUid(); - + target.account = message.getFolder().getAccountUuid(); + target.uri = message.getUri(); } catch (MessagingException me) { Log.w(K9.LOG_TAG, "Unable to load message info", me); } @@ -86,11 +99,11 @@ public class MessageHelper { CharSequence displayName; if (fromAddrs.length > 0 && account.isAnIdentity(fromAddrs[0])) { - CharSequence to = Address.toFriendly(toAddrs, contactHelper); + CharSequence to = toFriendly(toAddrs, contactHelper); displayName = new SpannableStringBuilder( mContext.getString(R.string.message_to_label)).append(to); } else { - displayName = Address.toFriendly(fromAddrs, contactHelper); + displayName = toFriendly(fromAddrs, contactHelper); } return displayName; @@ -104,4 +117,69 @@ public class MessageHelper { } return false; } + + /** + * Returns the name of the contact this email address belongs to if + * the {@link Contacts contacts} parameter is not {@code null} and a + * contact is found. Otherwise the personal portion of the {@link Address} + * is returned. If that isn't available either, the email address is + * returned. + * + * @param address An {@link com.fsck.k9.mail.Address} + * @param contacts A {@link Contacts} instance or {@code null}. + * @return A "friendly" name for this {@link Address}. + */ + public static CharSequence toFriendly(Address address, Contacts contacts) { + return toFriendly(address,contacts, + K9.showCorrespondentNames(), + K9.changeContactNameColor(), + K9.getContactNameColor()); + } + + public static CharSequence toFriendly(Address[] addresses, Contacts contacts) { + if (addresses == null) { + return null; + } + + if (addresses.length >= TOO_MANY_ADDRESSES) { + // Don't look up contacts if the number of addresses is very high. + contacts = null; + } + + SpannableStringBuilder sb = new SpannableStringBuilder(); + for (int i = 0; i < addresses.length; i++) { + sb.append(toFriendly(addresses[i], contacts)); + if (i < addresses.length - 1) { + sb.append(','); + } + } + return sb; + } + + /* package, for testing */ static CharSequence toFriendly(Address address, Contacts contacts, + boolean showCorrespondentNames, + boolean changeContactNameColor, + int contactNameColor) { + if (!showCorrespondentNames) { + return address.getAddress(); + } else if (contacts != null) { + final String name = contacts.getNameForAddress(address.getAddress()); + // TODO: The results should probably be cached for performance reasons. + if (name != null) { + if (changeContactNameColor) { + final SpannableString coloredName = new SpannableString(name); + coloredName.setSpan(new ForegroundColorSpan(contactNameColor), + 0, + coloredName.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + return coloredName; + } else { + return name; + } + } + } + + return (!TextUtils.isEmpty(address.getPersonal())) ? address.getPersonal() : address.getAddress(); + } } diff --git a/src/com/fsck/k9/helper/Regex.java b/src/com/fsck/k9/helper/Regex.java deleted file mode 100644 index dd60134a0..000000000 --- a/src/com/fsck/k9/helper/Regex.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Imported from AOSP on 2011-01-12 by JRV. - * Domain patterns updated from IANA on 2010-01-12 - * - * - */ - -package com.fsck.k9.helper; - -import java.util.regex.Pattern; - -/** - * Commonly used regular expression patterns. - */ -public class Regex { - - /** - * Goegular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2011/01/12. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py - */ - public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = - "(?:" - + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" - + "|(?:biz|b[abdefghijmnorstvwyz])" - + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" - + "|d[ejkmoz]" - + "|(?:edu|e[cegrstu])" - + "|f[ijkmor]" - + "|(?:gov|g[abdefghilmnpqrstuwy])" - + "|h[kmnrtu]" - + "|(?:info|int|i[delmnoqrst])" - + "|(?:jobs|j[emop])" - + "|k[eghimnprwyz]" - + "|l[abcikrstuvy]" - + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" - + "|(?:name|net|n[acefgilopruz])" - + "|(?:org|om)" - + "|(?:pro|p[aefghklmnrstwy])" - + "|qa" - + "|r[eosuw]" - + "|s[abcdeghijklmnortuvyz]" - + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" - + "|u[agksyz]" - + "|v[aceginu]" - + "|w[fs]" - + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah)" - + "|y[et]" - + "|z[amw]))"; - - /* This comprises most common used Unicode characters allowed in IRI - * as detailed in RFC 3987. - * Specifically, those two byte Unicode characters are not included. - */ - public static final String GOOD_IRI_CHAR = - "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; - - /** - * Regular expression pattern to match most part of RFC 3987 - * Internationalized URLs, aka IRIs. Commonly used Unicode characters are - * added. - */ - public static final Pattern WEB_URL_PATTERN = Pattern.compile( - "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" - + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" - + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" - + "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host - + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL - + "|(?:(?:25[0-5]|2[0-4]" // or ip address - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" - + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9])))" - + "(?:\\:\\d{1,5})?)" // plus option port number - + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params - + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" - + "(?:\\b|$)"); // and finally, a word boundary or end of - // input. This is to stop foo.sure from - // matching as foo.su - - public static final Pattern EMAIL_ADDRESS_PATTERN - = Pattern.compile( - "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + - "\\@" + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + - "(" + - "\\." + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + - ")+" - ); - - public static final String BITCOIN_URI_PATTERN = - "bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?"; -} diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java index 50ca99e3a..ffd6270a2 100644 --- a/src/com/fsck/k9/helper/Utility.java +++ b/src/com/fsck/k9/helper/Utility.java @@ -15,7 +15,6 @@ import android.widget.EditText; import android.widget.TextView; import com.fsck.k9.K9; -import com.fsck.k9.mail.filter.Base64; import java.nio.charset.Charset; import java.util.ArrayList; @@ -91,22 +90,6 @@ public class Utility { return TextUtils.join(String.valueOf(separator), parts); } - public static String base64Decode(String encoded) { - if (encoded == null) { - return null; - } - byte[] decoded = new Base64().decode(encoded.getBytes()); - return new String(decoded); - } - - public static String base64Encode(String s) { - if (s == null) { - return s; - } - byte[] encoded = new Base64().encode(s.getBytes()); - return new String(encoded); - } - public static boolean requiredFieldValid(TextView view) { return view.getText() != null && view.getText().length() > 0; } @@ -130,48 +113,6 @@ public class Utility { return false; } - private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$"); - - /** - * Quote a string, if necessary, based upon the definition of an "atom," as defined by RFC2822 - * (http://tools.ietf.org/html/rfc2822#section-3.2.4). Strings that consist purely of atoms are - * left unquoted; anything else is returned as a quoted string. - * @param text String to quote. - * @return Possibly quoted string. - */ - public static String quoteAtoms(final String text) { - if (ATOM.matcher(text).matches()) { - return text; - } else { - return quoteString(text); - } - } - - /** - * Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the - * double quote character to start and end if it's not already there. - * sample -> "sample" - * "sample" -> "sample" - * ""sample"" -> "sample" - * "sample"" -> "sample" - * sa"mp"le -> "sa"mp"le" - * "sa"mp"le" -> "sa"mp"le" - * (empty string) -> "" - * " -> "" - * @param s - * @return - */ - public static String quoteString(String s) { - if (s == null) { - return null; - } - if (!s.matches("^\".*\"$")) { - return "\"" + s + "\""; - } else { - return s; - } - } - /** * A fast version of URLDecoder.decode() that works only with UTF-8 and does only two * allocations. This version is around 3x as fast as the standard one and I'm using it diff --git a/src/com/fsck/k9/mail/Address.java b/src/com/fsck/k9/mail/Address.java index f3810f051..38a5a0f0b 100644 --- a/src/com/fsck/k9/mail/Address.java +++ b/src/com/fsck/k9/mail/Address.java @@ -3,6 +3,7 @@ package com.fsck.k9.mail; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.EncoderUtil; @@ -10,33 +11,15 @@ import org.apache.james.mime4j.dom.address.Mailbox; import org.apache.james.mime4j.dom.address.MailboxList; import org.apache.james.mime4j.field.address.AddressBuilder; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; import android.util.Log; -import com.fsck.k9.K9; -import com.fsck.k9.helper.Contacts; -import com.fsck.k9.helper.Utility; - +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; public class Address { - /** - * If the number of addresses exceeds this value the addresses aren't - * resolved to the names of Android contacts. - * - *

    - * TODO: This number was chosen arbitrarily and should be determined by - * performance tests. - *

    - * - * @see Address#toFriendly(Address[], Contacts) - */ - private static final int TOO_MANY_ADDRESSES = 50; + private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$"); /** * Immutable empty {@link Address} array @@ -162,12 +145,12 @@ public class Address { Mailbox mailbox = (Mailbox)address; addresses.add(new Address(mailbox.getLocalPart() + "@" + mailbox.getDomain(), mailbox.getName(), false)); } else { - Log.e(K9.LOG_TAG, "Unknown address type from Mime4J: " + Log.e(LOG_TAG, "Unknown address type from Mime4J: " + address.getClass().toString()); } } } catch (MimeException pe) { - Log.e(K9.LOG_TAG, "MimeException in Address.parse()", pe); + Log.e(LOG_TAG, "MimeException in Address.parse()", pe); //but we do an silent failover : we just use the given string as name with empty address addresses.add(new Address(null, addressList, false)); } @@ -198,7 +181,7 @@ public class Address { @Override public String toString() { if (!TextUtils.isEmpty(mPersonal)) { - return Utility.quoteAtoms(mPersonal) + " <" + mAddress + ">"; + return quoteAtoms(mPersonal) + " <" + mAddress + ">"; } else { return mAddress; } @@ -233,77 +216,6 @@ public class Address { return sb.toString(); } - /** - * Returns either the personal portion of the Address or the address portion if the personal - * is not available. - * @return - */ - public CharSequence toFriendly() { - return toFriendly((Contacts)null); - } - - /** - * Returns the name of the contact this email address belongs to if - * the {@link Contacts contacts} parameter is not {@code null} and a - * contact is found. Otherwise the personal portion of the {@link Address} - * is returned. If that isn't available either, the email address is - * returned. - * - * @param contacts - * A {@link Contacts} instance or {@code null}. - * @return - * A "friendly" name for this {@link Address}. - */ - public CharSequence toFriendly(final Contacts contacts) { - if (!K9.showCorrespondentNames()) { - return mAddress; - - } else if (contacts != null) { - final String name = contacts.getNameForAddress(mAddress); - - // TODO: The results should probably be cached for performance reasons. - - if (name != null) { - if (K9.changeContactNameColor()) { - final SpannableString coloredName = new SpannableString(name); - coloredName.setSpan(new ForegroundColorSpan(K9.getContactNameColor()), - 0, - coloredName.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - return coloredName; - } else { - return name; - } - } - } - - return (!TextUtils.isEmpty(mPersonal)) ? mPersonal : mAddress; - } - - public static CharSequence toFriendly(Address[] addresses) { - return toFriendly(addresses, null); - } - - public static CharSequence toFriendly(Address[] addresses, Contacts contacts) { - if (addresses == null) { - return null; - } - - if (addresses.length >= TOO_MANY_ADDRESSES) { - // Don't look up contacts if the number of addresses is very high. - contacts = null; - } - - SpannableStringBuilder sb = new SpannableStringBuilder(); - for (int i = 0; i < addresses.length; i++) { - sb.append(addresses[i].toFriendly(contacts)); - if (i < addresses.length - 1) { - sb.append(','); - } - } - return sb; - } /** * Unpacks an address list previously packed with packAddressList() @@ -368,4 +280,44 @@ public class Address { } return sb.toString(); } + + /** + * Quote a string, if necessary, based upon the definition of an "atom," as defined by RFC2822 + * (http://tools.ietf.org/html/rfc2822#section-3.2.4). Strings that consist purely of atoms are + * left unquoted; anything else is returned as a quoted string. + * @param text String to quote. + * @return Possibly quoted string. + */ + public static String quoteAtoms(final String text) { + if (ATOM.matcher(text).matches()) { + return text; + } else { + return quoteString(text); + } + } + + /** + * Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the + * double quote character to start and end if it's not already there. + * sample -> "sample" + * "sample" -> "sample" + * ""sample"" -> "sample" + * "sample"" -> "sample" + * sa"mp"le -> "sa"mp"le" + * "sa"mp"le" -> "sa"mp"le" + * (empty string) -> "" + * " -> "" + * @param s + * @return + */ + private static String quoteString(String s) { + if (s == null) { + return null; + } + if (!s.matches("^\".*\"$")) { + return "\"" + s + "\""; + } else { + return s; + } + } } diff --git a/src/com/fsck/k9/mail/Body.java b/src/com/fsck/k9/mail/Body.java index 93960892c..d86746d66 100644 --- a/src/com/fsck/k9/mail/Body.java +++ b/src/com/fsck/k9/mail/Body.java @@ -5,8 +5,6 @@ import java.io.IOException; import java.io.InputStream; 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. @@ -18,7 +16,7 @@ public interface Body { /** * Sets the content transfer encoding (7bit, 8bit, quoted-printable or base64). */ - public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException; + public void setEncoding(String encoding) throws MessagingException; /** * Writes the body's data to the given {@link OutputStream}. diff --git a/src/com/fsck/k9/mail/BodyPart.java b/src/com/fsck/k9/mail/BodyPart.java index 1118304ba..551866829 100644 --- a/src/com/fsck/k9/mail/BodyPart.java +++ b/src/com/fsck/k9/mail/BodyPart.java @@ -1,6 +1,6 @@ - package com.fsck.k9.mail; + public abstract class BodyPart implements Part { private Multipart mParent; @@ -13,7 +13,4 @@ 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/Folder.java b/src/com/fsck/k9/mail/Folder.java index 88af1a31a..5502ef259 100644 --- a/src/com/fsck/k9/mail/Folder.java +++ b/src/com/fsck/k9/mail/Folder.java @@ -1,21 +1,15 @@ 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; -import com.fsck.k9.K9; -import com.fsck.k9.Preferences; -import com.fsck.k9.controller.MessageRetrievalListener; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; -public abstract class Folder { - protected final Account mAccount; - +public abstract class Folder { private String status = null; private long lastChecked = 0; private long lastPush = 0; @@ -32,10 +26,6 @@ public abstract class Folder { HOLDS_FOLDERS, HOLDS_MESSAGES, } - protected Folder(Account account) { - mAccount = account; - } - /** * Forces an open of the MailProvider. If the provider is already open this * function returns without doing anything. @@ -83,7 +73,7 @@ public abstract class Folder { public abstract int getUnreadMessageCount() throws MessagingException; public abstract int getFlaggedMessageCount() throws MessagingException; - public abstract Message getMessage(String uid) throws MessagingException; + public abstract T getMessage(String uid) throws MessagingException; /** * Fetch the shells of messages between a range of UIDs and after a given date. @@ -94,7 +84,7 @@ public abstract class Folder { * @return List of messages * @throws MessagingException */ - public abstract List 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 @@ -104,13 +94,13 @@ public abstract class Folder { * @param listener Listener to notify as we download messages. * @return List of messages */ - public abstract List getMessages(MessageRetrievalListener listener) throws MessagingException; + public abstract List getMessages(MessageRetrievalListener listener) throws MessagingException; - public List getMessages(MessageRetrievalListener listener, boolean includeDeleted) throws MessagingException { + public List getMessages(MessageRetrievalListener listener, boolean includeDeleted) throws MessagingException { return getMessages(listener); } - public abstract List getMessages(String[] uids, MessageRetrievalListener listener) + public abstract List getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; public abstract Map appendMessages(List messages) throws MessagingException; @@ -149,15 +139,15 @@ public abstract class Folder { * @throws MessagingException */ public abstract void fetch(List messages, FetchProfile fp, - MessageRetrievalListener listener) throws MessagingException; + MessageRetrievalListener listener) throws MessagingException; public void fetchPart(Message message, Part part, - MessageRetrievalListener listener) throws MessagingException { + MessageRetrievalListener listener) throws MessagingException { // This is causing trouble. Disabled for now. See issue 1733 //throw new RuntimeException("fetchPart() not implemented."); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "fetchPart() not implemented."); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "fetchPart() not implemented."); } public abstract void delete(boolean recurse) throws MessagingException; @@ -225,10 +215,6 @@ public abstract class Folder { return getSyncClass(); } - public void refresh(Preferences preferences) throws MessagingException { - - } - public boolean isInTopGroup() { return false; } @@ -241,10 +227,6 @@ public abstract class Folder { this.status = status; } - public Account getAccount() { - return mAccount; - } - 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/K9MailLib.java b/src/com/fsck/k9/mail/K9MailLib.java new file mode 100644 index 000000000..490b5e67a --- /dev/null +++ b/src/com/fsck/k9/mail/K9MailLib.java @@ -0,0 +1,44 @@ +package com.fsck.k9.mail; + +import com.fsck.k9.K9; + +public class K9MailLib { + private K9MailLib() {} + + public static final String LOG_TAG = K9.LOG_TAG; + + public static final int PUSH_WAKE_LOCK_TIMEOUT = K9.PUSH_WAKE_LOCK_TIMEOUT; + public static final String IDENTITY_HEADER = K9.IDENTITY_HEADER; + + /** + * Should K-9 log the conversation it has over the wire with + * SMTP servers? + */ + public static boolean DEBUG_PROTOCOL_SMTP = true; + + /** + * Should K-9 log the conversation it has over the wire with + * IMAP servers? + */ + public static boolean DEBUG_PROTOCOL_IMAP = true; + + /** + * Should K-9 log the conversation it has over the wire with + * POP3 servers? + */ + public static boolean DEBUG_PROTOCOL_POP3 = true; + + /** + * Should K-9 log the conversation it has over the wire with + * WebDAV servers? + */ + public static boolean DEBUG_PROTOCOL_WEBDAV = true; + + public static boolean isDebug() { + return K9.DEBUG; + } + + public static boolean isDebugSensitive() { + return K9.DEBUG_SENSITIVE; + } +} diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 5a4d7a34a..45c85450f 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -2,26 +2,20 @@ 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; import android.util.Log; -import com.fsck.k9.K9; -import com.fsck.k9.activity.MessageReference; import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; -import com.fsck.k9.mail.store.UnavailableStorageException; + +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; public abstract class Message implements Part, CompositeBody { - - private MessageReference mReference = null; - public enum RecipientType { TO, CC, BCC, } @@ -54,9 +48,8 @@ public abstract class Message implements Part, CompositeBody { return false; } Message other = (Message)o; - return (mUid.equals(other.getUid()) - && mFolder.getName().equals(other.getFolder().getName()) - && mFolder.getAccount().getUuid().equals(other.getFolder().getAccount().getUuid())); + return (getUid().equals(other.getUid()) + && getFolder().getName().equals(other.getFolder().getName())); } @Override @@ -65,7 +58,6 @@ public abstract class Message implements Part, CompositeBody { int result = 1; result = MULTIPLIER * result + mFolder.getName().hashCode(); - result = MULTIPLIER * result + mFolder.getAccount().getUuid().hashCode(); result = MULTIPLIER * result + mUid.hashCode(); return result; } @@ -75,7 +67,6 @@ public abstract class Message implements Part, CompositeBody { } public void setUid(String uid) { - mReference = null; this.mUid = uid; } @@ -97,7 +88,7 @@ public abstract class Message implements Part, CompositeBody { public abstract Date getSentDate(); - public abstract void setSentDate(Date sentDate) throws MessagingException; + public abstract void setSentDate(Date sentDate, boolean hideTimeZone) throws MessagingException; public abstract Address[] getRecipients(RecipientType type) throws MessagingException; @@ -126,28 +117,7 @@ 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 Set getHeaderNames() throws MessagingException; public abstract long getId(); @@ -249,20 +219,10 @@ 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 setEncoding(String encoding) throws MessagingException; public abstract void setCharset(String charset) throws MessagingException; - public MessageReference makeMessageReference() { - if (mReference == null) { - mReference = new MessageReference(); - mReference.accountUuid = getFolder().getAccount().getUuid(); - mReference.folderName = getFolder().getName(); - mReference.uid = mUid; - } - return mReference; - } - public long calculateSize() { try { @@ -272,9 +232,9 @@ public abstract class Message implements Part, CompositeBody { eolOut.flush(); return out.getCount(); } catch (IOException e) { - Log.e(K9.LOG_TAG, "Failed to calculate a message size", e); + Log.e(LOG_TAG, "Failed to calculate a message size", e); } catch (MessagingException e) { - Log.e(K9.LOG_TAG, "Failed to calculate a message size", e); + Log.e(LOG_TAG, "Failed to calculate a message size", e); } return 0; } @@ -282,14 +242,12 @@ public abstract class Message implements Part, CompositeBody { /** * Copy the contents of this object into another {@code Message} object. * - * @param destination - * The {@code Message} object to receive the contents of this instance. + * @param destination The {@code Message} object to receive the contents of this instance. */ protected void copy(Message destination) { destination.mUid = mUid; destination.mInternalDate = mInternalDate; destination.mFolder = mFolder; - destination.mReference = mReference; // mFlags contents can change during the object lifetime, so copy the Set destination.mFlags = EnumSet.copyOf(mFlags); @@ -308,6 +266,5 @@ public abstract class Message implements Part, CompositeBody { */ @Override public abstract Message clone(); - @Override - public abstract void setUsing7bitTransport() throws MessagingException; + } diff --git a/src/com/fsck/k9/controller/MessageRetrievalListener.java b/src/com/fsck/k9/mail/MessageRetrievalListener.java similarity index 60% rename from src/com/fsck/k9/controller/MessageRetrievalListener.java rename to src/com/fsck/k9/mail/MessageRetrievalListener.java index 24ecb52e2..0d6b4fff1 100644 --- a/src/com/fsck/k9/controller/MessageRetrievalListener.java +++ b/src/com/fsck/k9/mail/MessageRetrievalListener.java @@ -1,12 +1,11 @@ -package com.fsck.k9.controller; +package com.fsck.k9.mail; -import com.fsck.k9.mail.Message; -public interface MessageRetrievalListener { +public interface MessageRetrievalListener { public void messageStarted(String uid, int number, int ofTotal); - public void messageFinished(Message message, int number, int ofTotal); + public void messageFinished(T message, int number, int ofTotal); /** * FIXME this method is almost never invoked by various Stores! Don't rely on it unless fixed!! diff --git a/src/com/fsck/k9/mail/Multipart.java b/src/com/fsck/k9/mail/Multipart.java index 567b0902d..8bcba92b3 100644 --- a/src/com/fsck/k9/mail/Multipart.java +++ b/src/com/fsck/k9/mail/Multipart.java @@ -7,7 +7,7 @@ import java.util.List; import org.apache.james.mime4j.util.MimeUtil; -import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.CharsetSupport; import com.fsck.k9.mail.internet.TextBody; public abstract class Multipart implements CompositeBody { @@ -64,7 +64,7 @@ public abstract class Multipart implements CompositeBody { BodyPart part = mParts.get(0); Body body = part.getBody(); if (body instanceof TextBody) { - MimeUtility.setCharset(charset, part); + CharsetSupport.setCharset(charset, part); ((TextBody)body).setCharset(charset); } } diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java index 38c1ad9da..4c692b724 100644 --- a/src/com/fsck/k9/mail/Part.java +++ b/src/com/fsck/k9/mail/Part.java @@ -5,29 +5,29 @@ import java.io.IOException; import java.io.OutputStream; public interface Part { - public void addHeader(String name, String value) throws MessagingException; + void addHeader(String name, String value) throws MessagingException; - public void removeHeader(String name) throws MessagingException; + void removeHeader(String name) throws MessagingException; - public void setHeader(String name, String value) throws MessagingException; + void setHeader(String name, String value) throws MessagingException; - public Body getBody(); + Body getBody(); - public String getContentType() throws MessagingException; + String getContentType() throws MessagingException; - public String getDisposition() throws MessagingException; + String getDisposition() throws MessagingException; - public String getContentId() throws MessagingException; + String getContentId() throws MessagingException; - public String[] getHeader(String name) throws MessagingException; + String[] getHeader(String name) throws MessagingException; - public boolean isMimeType(String mimeType) throws MessagingException; + boolean isMimeType(String mimeType) throws MessagingException; - public String getMimeType() throws MessagingException; + String getMimeType() throws MessagingException; - public void setBody(Body body) throws MessagingException; + void setBody(Body body) throws MessagingException; - public void writeTo(OutputStream out) throws IOException, MessagingException; + void writeTo(OutputStream out) throws IOException, MessagingException; /** * Called just prior to transmission, once the type of transport is known to @@ -41,5 +41,5 @@ public interface Part { * */ //TODO perhaps it would be clearer to use a flag "force7bit" in writeTo - public abstract void setUsing7bitTransport() throws MessagingException; + void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/ServerSettings.java b/src/com/fsck/k9/mail/ServerSettings.java index 43e16847b..59633ffdd 100644 --- a/src/com/fsck/k9/mail/ServerSettings.java +++ b/src/com/fsck/k9/mail/ServerSettings.java @@ -3,7 +3,6 @@ package com.fsck.k9.mail; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import com.fsck.k9.Account; /** * This is an abstraction to get rid of the store- and transport-specific URIs. @@ -13,8 +12,8 @@ import com.fsck.k9.Account; * store/transport URIs altogether. *

    * - * @see Account#getStoreUri() - * @see Account#getTransportUri() + * @see com.fsck.k9.mail.store.StoreConfig#getStoreUri() + * @see com.fsck.k9.mail.store.StoreConfig#getTransportUri() */ public class ServerSettings { /** diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 7dbfade7e..0892279c5 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -1,24 +1,10 @@ package com.fsck.k9.mail; -import java.util.HashMap; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; 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; -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.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; /** * Store is the access point for an email message store. It's location can be @@ -29,191 +15,6 @@ import com.fsck.k9.mail.store.WebDavStore; * making as few network connections as possible. */ public abstract class Store { - protected static final int SOCKET_CONNECT_TIMEOUT = 30000; - protected static final int SOCKET_READ_TIMEOUT = 60000; - - /** - * Remote stores indexed by Uri. - */ - 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 ConcurrentMap sLocalStores = new ConcurrentHashMap(); - - /** - * Lock objects indexed by account UUID. - * - * @see #getLocalInstance(Account, Application) - */ - private static ConcurrentMap sAccountLocks = new ConcurrentHashMap(); - - /** - * Get an instance of a remote mail store. - */ - public synchronized static Store getRemoteInstance(Account account) throws MessagingException { - String uri = account.getStoreUri(); - - if (uri.startsWith("local")) { - throw new RuntimeException("Asked to get non-local Store object but given LocalStore URI"); - } - - Store store = sStores.get(uri); - if (store == null) { - if (uri.startsWith("imap")) { - store = new ImapStore(account); - } else if (uri.startsWith("pop3")) { - store = new Pop3Store(account); - } else if (uri.startsWith("webdav")) { - store = new WebDavStore(account); - } - - if (store != null) { - sStores.put(uri, store); - } - } - - if (store == null) { - throw new MessagingException("Unable to locate an applicable Store for " + uri); - } - - return store; - } - - /** - * Get an instance of a local mail store. - * - * @throws UnavailableStorageException - * if not {@link StorageProvider#isReady(Context)} - */ - public static LocalStore getLocalInstance(Account account, Application application) - throws MessagingException { - - String accountUuid = account.getUuid(); - - // Create new per-account lock object if necessary - sAccountLocks.putIfAbsent(accountUuid, new Object()); - - // Get the account's lock object - Object lock = sAccountLocks.get(accountUuid); - - // Use per-account locks so DatabaseUpgradeService always knows which account database is - // currently upgraded. - synchronized (lock) { - Store store = sLocalStores.get(accountUuid); - - if (store == null) { - // Creating a LocalStore instance will create or upgrade the database if - // necessary. This could take some time. - store = new LocalStore(account, application); - - sLocalStores.put(accountUuid, store); - } - - return (LocalStore) store; - } - } - - public static void removeAccount(Account account) { - try { - removeRemoteInstance(account); - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Failed to reset remote store for account " + account.getUuid(), e); - } - - try { - removeLocalInstance(account); - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Failed to reset local store for account " + account.getUuid(), e); - } - } - - /** - * Release reference to a local mail store instance. - * - * @param account - * {@link Account} instance that is used to get the local mail store instance. - */ - private static void removeLocalInstance(Account account) { - String accountUuid = account.getUuid(); - sLocalStores.remove(accountUuid); - } - - /** - * Release reference to a remote mail store instance. - * - * @param account - * {@link Account} instance that is used to get the remote mail store instance. - */ - private synchronized static void removeRemoteInstance(Account account) { - String uri = account.getStoreUri(); - - if (uri.startsWith("local")) { - throw new RuntimeException("Asked to get non-local Store object but given " + - "LocalStore URI"); - } - - sStores.remove(uri); - } - - /** - * Decodes the contents of store-specific URIs and puts them into a {@link ServerSettings} - * object. - * - * @param uri - * the store-specific URI to decode - * - * @return A {@link ServerSettings} object holding the settings contained in the URI. - * - * @see ImapStore#decodeUri(String) - * @see Pop3Store#decodeUri(String) - * @see WebDavStore#decodeUri(String) - */ - public static ServerSettings decodeStoreUri(String uri) { - if (uri.startsWith("imap")) { - return ImapStore.decodeUri(uri); - } else if (uri.startsWith("pop3")) { - return Pop3Store.decodeUri(uri); - } else if (uri.startsWith("webdav")) { - return WebDavStore.decodeUri(uri); - } else { - throw new IllegalArgumentException("Not a valid store URI"); - } - } - - /** - * Creates a store URI from the information supplied in the {@link ServerSettings} object. - * - * @param server - * The {@link ServerSettings} object that holds the server settings. - * - * @return A store URI that holds the same information as the {@code server} parameter. - * - * @see ImapStore#createUri(ServerSettings) - * @see Pop3Store#createUri(ServerSettings) - * @see WebDavStore#createUri(ServerSettings) - */ - public static String createStoreUri(ServerSettings server) { - if (ImapStore.STORE_TYPE.equals(server.type)) { - return ImapStore.createUri(server); - } else if (Pop3Store.STORE_TYPE.equals(server.type)) { - return Pop3Store.createUri(server); - } else if (WebDavStore.STORE_TYPE.equals(server.type)) { - return WebDavStore.createUri(server); - } else { - throw new IllegalArgumentException("Not a valid store URI"); - } - } - - - protected final Account mAccount; - - - protected Store(Account account) { - mAccount = account; - } - public abstract Folder getFolder(String name); public abstract List getPersonalNamespaces(boolean forceListAll) throws MessagingException; @@ -244,14 +45,25 @@ public abstract class Store { return true; } - public void sendMessages(List messages) throws MessagingException { - } + public void sendMessages(List messages) throws MessagingException { } public Pusher getPusher(PushReceiver receiver) { return null; } - public Account getAccount() { - return mAccount; + protected static String decodeUtf8(String s) { + try { + return URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 not found"); + } + } + + protected static String encodeUtf8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 not found"); + } } } diff --git a/src/com/fsck/k9/mail/Transport.java b/src/com/fsck/k9/mail/Transport.java index 28011dacd..17d3e6866 100644 --- a/src/com/fsck/k9/mail/Transport.java +++ b/src/com/fsck/k9/mail/Transport.java @@ -1,22 +1,26 @@ package com.fsck.k9.mail; -import com.fsck.k9.Account; +import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mail.transport.SmtpTransport; import com.fsck.k9.mail.transport.WebDavTransport; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + public abstract class Transport { protected static final int SOCKET_CONNECT_TIMEOUT = 10000; // RFC 1047 protected static final int SOCKET_READ_TIMEOUT = 300000; - public synchronized static Transport getInstance(Account account) throws MessagingException { - String uri = account.getTransportUri(); + public synchronized static Transport getInstance(StoreConfig storeConfig) throws MessagingException { + String uri = storeConfig.getTransportUri(); if (uri.startsWith("smtp")) { - return new SmtpTransport(account); + return new SmtpTransport(storeConfig); } else if (uri.startsWith("webdav")) { - return new WebDavTransport(account); + return new WebDavTransport(storeConfig); } else { throw new MessagingException("Unable to locate an applicable Transport for " + uri); } @@ -71,4 +75,19 @@ public abstract class Transport { public abstract void sendMessage(Message message) throws MessagingException; public abstract void close(); + + protected static String encodeUtf8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 not found"); + } + } + protected static String decodeUtf8(String s) { + try { + return URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 not found"); + } + } } diff --git a/src/com/fsck/k9/mail/filter/Base64.java b/src/com/fsck/k9/mail/filter/Base64.java index 934c3b222..9f7cadfa5 100644 --- a/src/com/fsck/k9/mail/filter/Base64.java +++ b/src/com/fsck/k9/mail/filter/Base64.java @@ -34,6 +34,22 @@ import java.nio.charset.Charset; * @version $Id$ */ public class Base64 { + public static String decode(String encoded) { + if (encoded == null) { + return null; + } + byte[] decoded = new Base64().decode(encoded.getBytes()); + return new String(decoded); + } + + public static String encode(String s) { + if (s == null) { + return null; + } + byte[] encoded = new Base64().encode(s.getBytes()); + return new String(encoded); + } + /** * Chunk size per RFC 2045 section 6.8. * diff --git a/src/com/fsck/k9/mail/internet/CharsetSupport.java b/src/com/fsck/k9/mail/internet/CharsetSupport.java new file mode 100644 index 000000000..ffd241031 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/CharsetSupport.java @@ -0,0 +1,1121 @@ +package com.fsck.k9.mail.internet; + +import android.util.Log; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.util.Locale; + +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; +import static com.fsck.k9.mail.internet.JisSupport.SHIFT_JIS; + +public class CharsetSupport { + /** + * Table for character set fall-back. + * + * Table format: unsupported charset (regular expression), fall-back charset + */ + private static final String[][] CHARSET_FALLBACK_MAP = new String[][] { + // Some Android versions don't support KOI8-U + {"koi8-u", "koi8-r"}, + {"iso-2022-jp-[\\d]+", "iso-2022-jp"}, + // Default fall-back is US-ASCII + {".*", "US-ASCII"} + }; + + + public static void setCharset(String charset, Part part) throws MessagingException { + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + part.getMimeType() + ";\r\n charset=" + getExternalCharset(charset)); + } + + + public static String getCharsetFromAddress(String address) { + String variant = JisSupport.getJisVariantFromAddress(address); + if (variant != null) { + String charset = "x-" + variant + "-shift_jis-2007"; + if (Charset.isSupported(charset)) + return charset; + } + + return "UTF-8"; + } + + static String getExternalCharset(String charset) { + if (JisSupport.isShiftJis(charset)) { + return SHIFT_JIS; + } else { + return charset; + } + } + + static String fixupCharset(String charset, Message message) throws MessagingException { + if (charset == null || "0".equals(charset)) + charset = "US-ASCII"; // No encoding, so use us-ascii, which is the standard. + + charset = charset.toLowerCase(Locale.US); + if (charset.equals("cp932")) + charset = SHIFT_JIS; + + if (charset.equals(SHIFT_JIS) || charset.equals("iso-2022-jp")) { + String variant = JisSupport.getJisVariantFromMessage(message); + if (variant != null) + charset = "x-" + variant + "-" + charset + "-2007"; + } + return charset; + } + + + static String readToString(InputStream in, String charset) throws IOException { + boolean isIphoneString = false; + + // iso-2022-jp variants are supported by no versions as of Dec 2010. + if (charset.length() > 19 && charset.startsWith("x-") && + charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { + in = new Iso2022JpToShiftJisInputStream(in); + charset = "x-" + charset.substring(2, charset.length() - 17) + "-shift_jis-2007"; + } + + // shift_jis variants are supported by Eclair and later. + if (JisSupport.isShiftJis(charset) && !Charset.isSupported(charset)) { + // If the JIS variant is iPhone, map the Unicode private use area in iPhone to the one in Android after + // converting the character set from the standard Shift JIS to Unicode. + if (charset.substring(2, charset.length() - 15).equals("iphone")) + isIphoneString = true; + + charset = SHIFT_JIS; + } + + /* + * See if there is conversion from the MIME charset to the Java one. + * this function may also throw an exception if the charset name is not known + */ + boolean supported; + try { + supported = Charset.isSupported(charset); + } catch (IllegalCharsetNameException e) { + supported = false; + } + + for (String[] rule: CHARSET_FALLBACK_MAP) { + if (supported) { + break; + } + + if (charset.matches(rule[0])) { + Log.e(LOG_TAG, "I don't know how to deal with the charset " + charset + + ". Falling back to " + rule[1]); + charset = rule[1]; + try { + supported = Charset.isSupported(charset); + } catch (IllegalCharsetNameException e) { + supported = false; + } + } + } + + /* + * Convert and return as new String + */ + String str = IOUtils.toString(in, charset); + + if (isIphoneString) + str = importStringFromIphone(str); + return str; + } + + private static String importStringFromIphone(String str) { + StringBuilder buff = new StringBuilder(str.length()); + for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) { + int codePoint = str.codePointAt(i); + buff.appendCodePoint(importCodePointFromIphone(codePoint)); + } + return buff.toString(); + } + + private static int importCodePointFromIphone(int codePoint) { + switch (codePoint) { + case 0xE001: + return 0xFE19B; + case 0xE002: + return 0xFE19C; + case 0xE003: + return 0xFE823; + case 0xE004: + return 0xFE19D; + case 0xE005: + return 0xFE19E; + case 0xE006: + return 0xFE4CF; + case 0xE007: + return 0xFE4CD; + case 0xE008: + return 0xFE4EF; + case 0xE009: + return 0xFE523; + case 0xE00A: + return 0xFE525; + case 0xE00B: + return 0xFE528; + case 0xE00C: + return 0xFE538; + case 0xE00D: + return 0xFEB96; + case 0xE00E: + return 0xFEB97; + case 0xE00F: + return 0xFEB98; + case 0xE010: + return 0xFEB93; + case 0xE011: + return 0xFEB94; + case 0xE012: + return 0xFEB95; + case 0xE013: + return 0xFE7D5; + case 0xE014: + return 0xFE7D2; + case 0xE015: + return 0xFE7D3; + case 0xE016: + return 0xFE7D1; + case 0xE017: + return 0xFE7DA; + case 0xE018: + return 0xFE7D4; + case 0xE019: + return 0xFE1BD; + case 0xE01A: + return 0xFE1BE; + case 0xE01B: + return 0xFE7E4; + case 0xE01C: + return 0xFE7EA; + case 0xE01D: + return 0xFE7E9; + case 0xE01E: + return 0xFE7DF; + case 0xE01F: + return 0xFE7E3; + case 0xE020: + return 0xFEB09; + case 0xE021: + return 0xFEB04; + case 0xE022: + return 0xFEB0C; + case 0xE023: + return 0xFEB0E; + case 0xE024: + return 0xFE01E; + case 0xE025: + return 0xFE01F; + case 0xE026: + return 0xFE020; + case 0xE027: + return 0xFE021; + case 0xE028: + return 0xFE022; + case 0xE029: + return 0xFE023; + case 0xE02A: + return 0xFE024; + case 0xE02B: + return 0xFE025; + case 0xE02C: + return 0xFE026; + case 0xE02D: + return 0xFE027; + case 0xE02E: + return 0xFE028; + case 0xE02F: + return 0xFE029; + case 0xE030: + return 0xFE040; + case 0xE031: + return 0xFE4D2; + case 0xE032: + return 0xFE041; + case 0xE033: + return 0xFE512; + case 0xE034: + return 0xFE825; + case 0xE035: + return 0xFE826; + case 0xE036: + return 0xFE4B0; + case 0xE037: + return 0xFE4BB; + case 0xE038: + return 0xFE4B2; + case 0xE039: + return 0xFE7EC; + case 0xE03A: + return 0xFE7F5; + case 0xE03B: + return 0xFE4C3; + case 0xE03C: + return 0xFE800; + case 0xE03D: + return 0xFE801; + case 0xE03E: + return 0xFE813; + case 0xE03F: + return 0xFEB82; + case 0xE040: + return 0xFE815; + case 0xE041: + return 0xFE816; + case 0xE042: + return 0xFE818; + case 0xE043: + return 0xFE980; + case 0xE044: + return 0xFE982; + case 0xE045: + return 0xFE981; + case 0xE046: + return 0xFE962; + case 0xE047: + return 0xFE983; + case 0xE048: + return 0xFE003; + case 0xE049: + return 0xFE001; + case 0xE04A: + return 0xFE000; + case 0xE04B: + return 0xFE002; + case 0xE04C: + return 0xFE014; + case 0xE04D: + return 0xFE009; + case 0xE04E: + return 0xFE1AF; + case 0xE04F: + return 0xFE1B8; + case 0xE050: + return 0xFE1C0; + case 0xE051: + return 0xFE1C1; + case 0xE052: + return 0xFE1B7; + case 0xE053: + return 0xFE1C2; + case 0xE054: + return 0xFE1C3; + case 0xE055: + return 0xFE1BC; + case 0xE056: + return 0xFE335; + case 0xE057: + return 0xFE330; + case 0xE058: + return 0xFE323; + case 0xE059: + return 0xFE320; + case 0xE05A: + return 0xFE4F4; + case 0xE101: + return 0xFE52D; + case 0xE102: + return 0xFE52E; + case 0xE103: + return 0xFE52B; + case 0xE104: + return 0xFE526; + case 0xE105: + return 0xFE329; + case 0xE106: + return 0xFE327; + case 0xE107: + return 0xFE341; + case 0xE108: + return 0xFE344; + case 0xE109: + return 0xFE1C4; + case 0xE10A: + return 0xFE1C5; + case 0xE10B: + return 0xFE1BF; + case 0xE10C: + return 0xFE1B0; + case 0xE10D: + return 0xFE7ED; + case 0xE10E: + return 0xFE4D1; + case 0xE10F: + return 0xFEB56; + case 0xE110: + return 0xFE03C; + case 0xE111: + return 0xFE827; + case 0xE112: + return 0xFE510; + case 0xE113: + return 0xFE4F5; + case 0xE114: + return 0xFEB85; + case 0xE115: + return 0xFE7D9; + case 0xE116: + return 0xFE4CA; + case 0xE117: + return 0xFE515; + case 0xE118: + return 0xFE03F; + case 0xE119: + return 0xFE042; + case 0xE11A: + return 0xFE1B2; + case 0xE11B: + return 0xFE1AE; + case 0xE11C: + return 0xFE1B3; + case 0xE11D: + return 0xFE4F6; + case 0xE11E: + return 0xFE53B; + case 0xE11F: + return 0xFE537; + case 0xE120: + return 0xFE960; + case 0xE121: + return 0xFE4BC; + case 0xE122: + return 0xFE7FB; + case 0xE123: + return 0xFE7FA; + case 0xE124: + return 0xFE7FD; + case 0xE125: + return 0xFE807; + case 0xE126: + return 0xFE81D; + case 0xE127: + return 0xFE81E; + case 0xE128: + return 0xFE81F; + case 0xE129: + return 0xFE820; + case 0xE12A: + return 0xFE81C; + case 0xE12B: + return 0xFE1B1; + case 0xE12C: + return 0xFE81B; + case 0xE12D: + return 0xFE80B; + case 0xE12E: + return 0xFEB32; + case 0xE12F: + return 0xFE4DD; + case 0xE130: + return 0xFE80C; + case 0xE131: + return 0xFE7DB; + case 0xE132: + return 0xFE7D7; + case 0xE133: + return 0xFE80D; + case 0xE134: + return 0xFE7DC; + case 0xE135: + return 0xFE7EE; + case 0xE136: + return 0xFE7EB; + case 0xE137: + return 0xFE7F8; + case 0xE138: + return 0xFEB33; + case 0xE139: + return 0xFEB34; + case 0xE13A: + return 0xFEB35; + case 0xE13B: + return 0xFE509; + case 0xE13C: + return 0xFEB59; + case 0xE13D: + return 0xFE004; + case 0xE13E: + return 0xFE4D6; + case 0xE13F: + return 0xFE505; + case 0xE140: + return 0xFE507; + case 0xE141: + return 0xFE821; + case 0xE142: + return 0xFE52F; + case 0xE143: + return 0xFE514; + case 0xE144: + return 0xFEB86; + case 0xE145: + return 0xFEB87; + case 0xE146: + return 0xFE00B; + case 0xE147: + return 0xFE965; + case 0xE148: + return 0xFE546; + case 0xE149: + return 0xFE4DE; + case 0xE14A: + return 0xFE4DF; + case 0xE14B: + return 0xFE531; + case 0xE14C: + return 0xFEB5E; + case 0xE14D: + return 0xFE4B5; + case 0xE14E: + return 0xFE7F7; + case 0xE14F: + return 0xFE7F6; + case 0xE150: + return 0xFE7E7; + case 0xE151: + return 0xFE506; + case 0xE152: + return 0xFE1A1; + case 0xE153: + return 0xFE4B3; + case 0xE154: + return 0xFE4B6; + case 0xE155: + return 0xFE4B4; + case 0xE156: + return 0xFE4B9; + case 0xE157: + return 0xFE4BA; + case 0xE158: + return 0xFE4B7; + case 0xE159: + return 0xFE7E6; + case 0xE15A: + return 0xFE7EF; + case 0xE201: + return 0xFE7F0; + case 0xE202: + return 0xFE7E8; + case 0xE203: + return 0xFEB24; + case 0xE204: + return 0xFEB19; + case 0xE205: + return 0xFEB61; + case 0xE206: + return 0xFEB62; + case 0xE207: + return 0xFEB25; + case 0xE208: + return 0xFEB1F; + case 0xE209: + return 0xFE044; + case 0xE20A: + return 0xFEB20; + case 0xE20B: + return 0xFE838; + case 0xE20C: + return 0xFEB1A; + case 0xE20D: + return 0xFEB1C; + case 0xE20E: + return 0xFEB1B; + case 0xE20F: + return 0xFEB1D; + case 0xE210: + return 0xFE82C; + case 0xE211: + return 0xFE82B; + case 0xE212: + return 0xFEB36; + case 0xE213: + return 0xFEB37; + case 0xE214: + return 0xFEB38; + case 0xE215: + return 0xFEB39; + case 0xE216: + return 0xFEB3A; + case 0xE217: + return 0xFEB3B; + case 0xE218: + return 0xFEB3C; + case 0xE219: + return 0xFEB63; + case 0xE21A: + return 0xFEB64; + case 0xE21B: + return 0xFEB67; + case 0xE21C: + return 0xFE82E; + case 0xE21D: + return 0xFE82F; + case 0xE21E: + return 0xFE830; + case 0xE21F: + return 0xFE831; + case 0xE220: + return 0xFE832; + case 0xE221: + return 0xFE833; + case 0xE222: + return 0xFE834; + case 0xE223: + return 0xFE835; + case 0xE224: + return 0xFE836; + case 0xE225: + return 0xFE837; + case 0xE226: + return 0xFEB3D; + case 0xE227: + return 0xFEB3E; + case 0xE228: + return 0xFEB3F; + case 0xE229: + return 0xFEB81; + case 0xE22A: + return 0xFEB31; + case 0xE22B: + return 0xFEB2F; + case 0xE22C: + return 0xFEB40; + case 0xE22D: + return 0xFEB41; + case 0xE22E: + return 0xFEB99; + case 0xE22F: + return 0xFEB9A; + case 0xE230: + return 0xFEB9B; + case 0xE231: + return 0xFEB9C; + case 0xE232: + return 0xFEAF8; + case 0xE233: + return 0xFEAF9; + case 0xE234: + return 0xFEAFA; + case 0xE235: + return 0xFEAFB; + case 0xE236: + return 0xFEAF0; + case 0xE237: + return 0xFEAF2; + case 0xE238: + return 0xFEAF1; + case 0xE239: + return 0xFEAF3; + case 0xE23A: + return 0xFEAFC; + case 0xE23B: + return 0xFEAFD; + case 0xE23C: + return 0xFEAFE; + case 0xE23D: + return 0xFEAFF; + case 0xE23E: + return 0xFE4F8; + case 0xE23F: + return 0xFE02B; + case 0xE240: + return 0xFE02C; + case 0xE241: + return 0xFE02D; + case 0xE242: + return 0xFE02E; + case 0xE243: + return 0xFE02F; + case 0xE244: + return 0xFE030; + case 0xE245: + return 0xFE031; + case 0xE246: + return 0xFE032; + case 0xE247: + return 0xFE033; + case 0xE248: + return 0xFE034; + case 0xE249: + return 0xFE035; + case 0xE24A: + return 0xFE036; + case 0xE24B: + return 0xFE037; + case 0xE24C: + return 0xFEB42; + case 0xE24D: + return 0xFEB27; + case 0xE24E: + return 0xFEB29; + case 0xE24F: + return 0xFEB2D; + case 0xE250: + return 0xFE839; + case 0xE251: + return 0xFE83A; + case 0xE252: + return 0xFEB23; + case 0xE253: + return 0xFE1B4; + case 0xE254: + return 0xFEE77; + case 0xE255: + return 0xFEE78; + case 0xE256: + return 0xFEE79; + case 0xE257: + return 0xFEE7A; + case 0xE258: + return 0xFEE7B; + case 0xE259: + return 0xFEE7C; + case 0xE25A: + return 0xFEE7D; + case 0xE301: + return 0xFE527; + case 0xE302: + return 0xFE4D3; + case 0xE303: + return 0xFE045; + case 0xE304: + return 0xFE03D; + case 0xE305: + return 0xFE046; + case 0xE306: + return 0xFE828; + case 0xE307: + return 0xFE047; + case 0xE308: + return 0xFE048; + case 0xE309: + return 0xFE508; + case 0xE30A: + return 0xFE803; + case 0xE30B: + return 0xFE985; + case 0xE30C: + return 0xFE987; + case 0xE30D: + return 0xFEB43; + case 0xE30E: + return 0xFEB1E; + case 0xE30F: + return 0xFE50A; + case 0xE310: + return 0xFE516; + case 0xE311: + return 0xFEB58; + case 0xE312: + return 0xFE517; + case 0xE313: + return 0xFE53E; + case 0xE314: + return 0xFE50F; + case 0xE315: + return 0xFEB2B; + case 0xE316: + return 0xFE53C; + case 0xE317: + return 0xFE530; + case 0xE318: + return 0xFE4D4; + case 0xE319: + return 0xFE4D5; + case 0xE31A: + return 0xFE4D7; + case 0xE31B: + return 0xFE4D8; + case 0xE31C: + return 0xFE195; + case 0xE31D: + return 0xFE196; + case 0xE31E: + return 0xFE197; + case 0xE31F: + return 0xFE198; + case 0xE320: + return 0xFE199; + case 0xE321: + return 0xFE4D9; + case 0xE322: + return 0xFE4DA; + case 0xE323: + return 0xFE4F0; + case 0xE324: + return 0xFE808; + case 0xE325: + return 0xFE4F2; + case 0xE326: + return 0xFE814; + case 0xE327: + return 0xFEB0D; + case 0xE328: + return 0xFEB11; + case 0xE329: + return 0xFEB12; + case 0xE32A: + return 0xFEB13; + case 0xE32B: + return 0xFEB14; + case 0xE32C: + return 0xFEB15; + case 0xE32D: + return 0xFEB16; + case 0xE32E: + return 0xFEB60; + case 0xE32F: + return 0xFEB68; + case 0xE330: + return 0xFEB5D; + case 0xE331: + return 0xFEB5B; + case 0xE332: + return 0xFEB44; + case 0xE333: + return 0xFEB45; + case 0xE334: + return 0xFEB57; + case 0xE335: + return 0xFEB69; + case 0xE336: + return 0xFEB0A; + case 0xE337: + return 0xFEB0B; + case 0xE338: + return 0xFE984; + case 0xE339: + return 0xFE964; + case 0xE33A: + return 0xFE966; + case 0xE33B: + return 0xFE967; + case 0xE33C: + return 0xFE968; + case 0xE33D: + return 0xFE969; + case 0xE33E: + return 0xFE96A; + case 0xE33F: + return 0xFE96B; + case 0xE340: + return 0xFE963; + case 0xE341: + return 0xFE96C; + case 0xE342: + return 0xFE961; + case 0xE343: + return 0xFE96D; + case 0xE344: + return 0xFE96E; + case 0xE345: + return 0xFE051; + case 0xE346: + return 0xFE052; + case 0xE347: + return 0xFE053; + case 0xE348: + return 0xFE054; + case 0xE349: + return 0xFE055; + case 0xE34A: + return 0xFE056; + case 0xE34B: + return 0xFE511; + case 0xE34C: + return 0xFE96F; + case 0xE34D: + return 0xFE970; + case 0xE401: + return 0xFE345; + case 0xE402: + return 0xFE343; + case 0xE403: + return 0xFE340; + case 0xE404: + return 0xFE333; + case 0xE405: + return 0xFE347; + case 0xE406: + return 0xFE33C; + case 0xE407: + return 0xFE33F; + case 0xE408: + return 0xFE342; + case 0xE409: + return 0xFE32A; + case 0xE40A: + return 0xFE33E; + case 0xE40B: + return 0xFE33B; + case 0xE40C: + return 0xFE32E; + case 0xE40D: + return 0xFE32F; + case 0xE40E: + return 0xFE326; + case 0xE40F: + return 0xFE325; + case 0xE410: + return 0xFE322; + case 0xE411: + return 0xFE33A; + case 0xE412: + return 0xFE334; + case 0xE413: + return 0xFE339; + case 0xE414: + return 0xFE336; + case 0xE415: + return 0xFE338; + case 0xE416: + return 0xFE33D; + case 0xE417: + return 0xFE32D; + case 0xE418: + return 0xFE32C; + case 0xE419: + return 0xFE190; + case 0xE41A: + return 0xFE192; + case 0xE41B: + return 0xFE191; + case 0xE41C: + return 0xFE193; + case 0xE41D: + return 0xFE35B; + case 0xE41E: + return 0xFEB9D; + case 0xE41F: + return 0xFEB9E; + case 0xE420: + return 0xFEB9F; + case 0xE421: + return 0xFEBA0; + case 0xE422: + return 0xFEBA1; + case 0xE423: + return 0xFE351; + case 0xE424: + return 0xFE352; + case 0xE425: + return 0xFE829; + case 0xE426: + return 0xFE353; + case 0xE427: + return 0xFE358; + case 0xE428: + return 0xFE1A0; + case 0xE429: + return 0xFE1A2; + case 0xE42A: + return 0xFE7D6; + case 0xE42B: + return 0xFE7DD; + case 0xE42C: + return 0xFE80E; + case 0xE42D: + return 0xFE7DE; + case 0xE42E: + return 0xFE7E5; + case 0xE42F: + return 0xFE7F1; + case 0xE430: + return 0xFE7F2; + case 0xE431: + return 0xFE7F3; + case 0xE432: + return 0xFE7F4; + case 0xE433: + return 0xFE7FE; + case 0xE434: + return 0xFE7E0; + case 0xE435: + return 0xFE7E2; + case 0xE436: + return 0xFE518; + case 0xE437: + return 0xFEB17; + case 0xE438: + return 0xFE519; + case 0xE439: + return 0xFE51A; + case 0xE43A: + return 0xFE51B; + case 0xE43B: + return 0xFE51C; + case 0xE43C: + return 0xFE007; + case 0xE43D: + return 0xFE82A; + case 0xE43E: + return 0xFE038; + case 0xE43F: + return 0xFE971; + case 0xE440: + return 0xFE51D; + case 0xE441: + return 0xFE1C6; + case 0xE442: + return 0xFE51E; + case 0xE443: + return 0xFE005; + case 0xE444: + return 0xFE049; + case 0xE445: + return 0xFE51F; + case 0xE446: + return 0xFE017; + case 0xE447: + return 0xFE043; + case 0xE448: + return 0xFE513; + case 0xE449: + return 0xFE00A; + case 0xE44A: + return 0xFE00C; + case 0xE44B: + return 0xFE008; + case 0xE44C: + return 0xFE00D; + case 0xE501: + return 0xFE4B8; + case 0xE502: + return 0xFE804; + case 0xE503: + return 0xFE805; + case 0xE504: + return 0xFE4BD; + case 0xE505: + return 0xFE4BE; + case 0xE506: + return 0xFE4BF; + case 0xE507: + return 0xFE802; + case 0xE508: + return 0xFE4C0; + case 0xE509: + return 0xFE4C4; + case 0xE50A: + return 0xFE4C5; + case 0xE50B: + return 0xFE4E5; + case 0xE50C: + return 0xFE4E6; + case 0xE50D: + return 0xFE4E7; + case 0xE50E: + return 0xFE4E8; + case 0xE50F: + return 0xFE4E9; + case 0xE510: + return 0xFE4EA; + case 0xE511: + return 0xFE4EB; + case 0xE512: + return 0xFE4EC; + case 0xE513: + return 0xFE4ED; + case 0xE514: + return 0xFE4EE; + case 0xE515: + return 0xFE1A4; + case 0xE516: + return 0xFE1A5; + case 0xE517: + return 0xFE1A6; + case 0xE518: + return 0xFE1A7; + case 0xE519: + return 0xFE1A8; + case 0xE51A: + return 0xFE1A9; + case 0xE51B: + return 0xFE1AA; + case 0xE51C: + return 0xFE1AB; + case 0xE51D: + return 0xFE4C6; + case 0xE51E: + return 0xFE1B5; + case 0xE51F: + return 0xFE1B6; + case 0xE520: + return 0xFE1C7; + case 0xE521: + return 0xFE1C8; + case 0xE522: + return 0xFE1C9; + case 0xE523: + return 0xFE1BA; + case 0xE524: + return 0xFE1CA; + case 0xE525: + return 0xFE1CB; + case 0xE526: + return 0xFE1CC; + case 0xE527: + return 0xFE1CD; + case 0xE528: + return 0xFE1CE; + case 0xE529: + return 0xFE1CF; + case 0xE52A: + return 0xFE1D0; + case 0xE52B: + return 0xFE1D1; + case 0xE52C: + return 0xFE1D2; + case 0xE52D: + return 0xFE1D3; + case 0xE52E: + return 0xFE1D4; + case 0xE52F: + return 0xFE1D5; + case 0xE530: + return 0xFE1D6; + case 0xE531: + return 0xFE1D7; + case 0xE532: + return 0xFE50B; + case 0xE533: + return 0xFE50C; + case 0xE534: + return 0xFE50D; + case 0xE535: + return 0xFE50E; + case 0xE536: + return 0xFE553; + case 0xE537: + return 0xFEB2A; + case 0xE538: + return 0xFEE70; + case 0xE539: + return 0xFEE71; + case 0xE53A: + return 0xFEE72; + case 0xE53B: + return 0xFEE73; + case 0xE53C: + return 0xFEE74; + case 0xE53D: + return 0xFEE75; + case 0xE53E: + return 0xFEE76; + default: + return codePoint; + } + } + +} diff --git a/src/com/fsck/k9/mail/internet/DecoderUtil.java b/src/com/fsck/k9/mail/internet/DecoderUtil.java index 3ac74947d..d17c83bc7 100644 --- a/src/com/fsck/k9/mail/internet/DecoderUtil.java +++ b/src/com/fsck/k9/mail/internet/DecoderUtil.java @@ -2,7 +2,6 @@ package com.fsck.k9.mail.internet; import android.util.Log; -import com.fsck.k9.K9; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import java.io.ByteArrayInputStream; @@ -13,6 +12,8 @@ import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.CharsetUtil; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + /** * Static methods for decoding strings, byte arrays and encoded words. @@ -35,7 +36,7 @@ class DecoderUtil { Base64InputStream is = new Base64InputStream(new ByteArrayInputStream(bytes)); try { - return MimeUtility.readToString(is, charset); + return CharsetSupport.readToString(is, charset); } catch (IOException e) { return null; } @@ -68,7 +69,7 @@ class DecoderUtil { QuotedPrintableInputStream is = new QuotedPrintableInputStream(new ByteArrayInputStream(bytes)); try { - return MimeUtility.readToString(is, charset); + return CharsetSupport.readToString(is, charset); } catch (IOException e) { return null; } @@ -162,13 +163,13 @@ class DecoderUtil { String charset; try { - charset = MimeUtility.fixupCharset(mimeCharset, message); + charset = CharsetSupport.fixupCharset(mimeCharset, message); } catch (MessagingException e) { return null; } if (encodedText.isEmpty()) { - Log.w(K9.LOG_TAG, "Missing encoded text in encoded word: '" + body.substring(begin, end) + "'"); + Log.w(LOG_TAG, "Missing encoded text in encoded word: '" + body.substring(begin, end) + "'"); return null; } @@ -177,7 +178,7 @@ class DecoderUtil { } else if (encoding.equalsIgnoreCase("B")) { return DecoderUtil.decodeB(encodedText, charset); } else { - Log.w(K9.LOG_TAG, "Warning: Unknown encoding in encoded word '" + body.substring(begin, end) + "'"); + Log.w(LOG_TAG, "Warning: Unknown encoding in encoded word '" + body.substring(begin, end) + "'"); return null; } } diff --git a/src/com/fsck/k9/mail/internet/EncoderUtil.java b/src/com/fsck/k9/mail/internet/EncoderUtil.java index 5e7672715..2c71ecc2d 100644 --- a/src/com/fsck/k9/mail/internet/EncoderUtil.java +++ b/src/com/fsck/k9/mail/internet/EncoderUtil.java @@ -68,7 +68,7 @@ class EncoderUtil { if (charset == null) charset = determineCharset(text); - String mimeCharset = MimeUtility.getExternalCharset(charset.name()); + String mimeCharset = CharsetSupport.getExternalCharset(charset.name()); byte[] bytes = encode(text, charset); diff --git a/src/com/fsck/k9/mail/internet/JisSupport.java b/src/com/fsck/k9/mail/internet/JisSupport.java new file mode 100644 index 000000000..a6a30329a --- /dev/null +++ b/src/com/fsck/k9/mail/internet/JisSupport.java @@ -0,0 +1,103 @@ +package com.fsck.k9.mail.internet; + +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; + +class JisSupport { + public static final String SHIFT_JIS = "shift_jis"; + + public static String getJisVariantFromMessage(Message message) throws MessagingException { + if (message == null) + return null; + + // If a receiver is known to use a JIS variant, the sender transfers the message after converting the + // charset as a convention. + String variant = getJisVariantFromReceivedHeaders(message); + if (variant != null) + return variant; + + // If a receiver is not known to use any JIS variants, the sender transfers the message without converting + // the charset. + variant = getJisVariantFromFromHeaders(message); + if (variant != null) + return variant; + + return getJisVariantFromMailerHeaders(message); + } + + public static boolean isShiftJis(String charset) { + return charset.length() > 17 && charset.startsWith("x-") + && charset.endsWith("-shift_jis-2007"); + } + + public static String getJisVariantFromAddress(String address) { + if (address == null) + return null; + if (isInDomain(address, "docomo.ne.jp") || isInDomain(address, "dwmail.jp") || + isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com") || + isInDomain(address, "emnet.ne.jp") || isInDomain(address, "emobile.ne.jp")) + return "docomo"; + else if (isInDomain(address, "softbank.ne.jp") || isInDomain(address, "vodafone.ne.jp") || + isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) + return "softbank"; + else if (isInDomain(address, "ezweb.ne.jp") || isInDomain(address, "ido.ne.jp")) + return "kddi"; + return null; + } + + + private static String getJisVariantFromMailerHeaders(Message message) throws MessagingException { + String mailerHeaders[] = message.getHeader("X-Mailer"); + if (mailerHeaders == null || mailerHeaders.length == 0) + return null; + + if (mailerHeaders[0].startsWith("iPhone Mail ") || mailerHeaders[0].startsWith("iPad Mail ")) + return "iphone"; + + return null; + } + + + private static String getJisVariantFromReceivedHeaders(Part message) throws MessagingException { + String receivedHeaders[] = message.getHeader("Received"); + if (receivedHeaders == null) + return null; + + for (String receivedHeader : receivedHeaders) { + String address = getAddressFromReceivedHeader(receivedHeader); + if (address == null) + continue; + String variant = getJisVariantFromAddress(address); + if (variant != null) + return variant; + } + return null; + } + + private static String getAddressFromReceivedHeader(String receivedHeader) { + // Not implemented yet! Extract an address from the FOR clause of the given Received header. + return null; + } + + private static String getJisVariantFromFromHeaders(Message message) throws MessagingException { + Address addresses[] = message.getFrom(); + if (addresses == null || addresses.length == 0) + return null; + + return getJisVariantFromAddress(addresses[0].getAddress()); + } + + private static boolean isInDomain(String address, String domain) { + int index = address.length() - domain.length() - 1; + if (index < 0) + return false; + + char c = address.charAt(index); + if (c != '@' && c != '.') + return false; + + return address.endsWith(domain); + } +} diff --git a/src/com/fsck/k9/mail/internet/MessageExtractor.java b/src/com/fsck/k9/mail/internet/MessageExtractor.java new file mode 100644 index 000000000..ba0bfa42f --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MessageExtractor.java @@ -0,0 +1,453 @@ +package com.fsck.k9.mail.internet; + +import android.util.Log; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; +import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset; +import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; +import static com.fsck.k9.mail.internet.Viewable.Alternative; +import static com.fsck.k9.mail.internet.Viewable.Html; +import static com.fsck.k9.mail.internet.Viewable.MessageHeader; +import static com.fsck.k9.mail.internet.Viewable.Text; +import static com.fsck.k9.mail.internet.Viewable.Textual; + +public class MessageExtractor { + private MessageExtractor() {} + + public static String getTextFromPart(Part part) { + try { + if ((part != null) && (part.getBody() != null)) { + final Body body = part.getBody(); + if (body instanceof TextBody) { + return ((TextBody)body).getText(); + } + + final String mimeType = part.getMimeType(); + if ((mimeType != null) && MimeUtility.mimeTypeMatches(mimeType, "text/*")) { + /* + * We've got a text part, so let's see if it needs to be processed further. + */ + String charset = getHeaderParameter(part.getContentType(), "charset"); + /* + * determine the charset from HTML message. + */ + if (mimeType.equalsIgnoreCase("text/html") && charset == null) { + InputStream in = part.getBody().getInputStream(); + try { + byte[] buf = new byte[256]; + in.read(buf, 0, buf.length); + String str = new String(buf, "US-ASCII"); + + if (str.isEmpty()) { + return ""; + } + Pattern p = Pattern.compile("", Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(str); + if (m.find()) { + charset = m.group(1); + } + } finally { + try { + if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) { + /* + * If this is a BinaryTempFileBodyInputStream, calling close() + * will delete the file. But we can't let that happen because + * the file needs to be opened again by the code a few lines + * down. + */ + ((BinaryTempFileBody.BinaryTempFileBodyInputStream) in).closeWithoutDeleting(); + } else { + in.close(); + } + } catch (Exception e) { /* ignore */ } + } + } + charset = fixupCharset(charset, getMessageFromPart(part)); + + /* + * Now we read the part into a buffer for further processing. Because + * the stream is now wrapped we'll remove any transfer encoding at this point. + */ + InputStream in = part.getBody().getInputStream(); + try { + String text = CharsetSupport.readToString(in, charset); + + // Replace the body with a TextBody that already contains the decoded text + part.setBody(new TextBody(text)); + + return text; + } finally { + try { + /* + * This time we don't care if it's a BinaryTempFileBodyInputStream. We + * replaced the body with a TextBody instance and hence don't need the + * file anymore. + */ + in.close(); + } catch (IOException e) { /* Ignore */ } + } + } + } + + } catch (OutOfMemoryError oom) { + /* + * If we are not able to process the body there's nothing we can do about it. Return + * null and let the upper layers handle the missing content. + */ + Log.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString()); + } catch (Exception e) { + /* + * If we are not able to process the body there's nothing we can do about it. Return + * null and let the upper layers handle the missing content. + */ + Log.e(LOG_TAG, "Unable to getTextFromPart", e); + } + return null; + } + + + /** + * Traverse the MIME tree of a message an extract viewable parts. + * + * @param part + * The message part to start from. + * @param attachments + * A list that will receive the parts that are considered attachments. + * + * @return A list of {@link Viewable}s. + * + * @throws MessagingException + * In case of an error. + */ + public static List getViewables(Part part, List attachments) throws MessagingException { + List viewables = new ArrayList(); + + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) { + /* + * For multipart/alternative parts we try to find a text/plain and a text/html + * child. Everything else we find is put into 'attachments'. + */ + List text = findTextPart(multipart, true); + + Set knownTextParts = getParts(text); + List html = findHtmlPart(multipart, knownTextParts, attachments, true); + + if (!text.isEmpty() || !html.isEmpty()) { + Alternative alternative = new Alternative(text, html); + viewables.add(alternative); + } + } else { + // For all other multipart parts we recurse to grab all viewable children. + for (Part bodyPart : multipart.getBodyParts()) { + viewables.addAll(getViewables(bodyPart, attachments)); + } + } + } else if (body instanceof Message && + !("attachment".equalsIgnoreCase(getContentDisposition(part)))) { + /* + * We only care about message/rfc822 parts whose Content-Disposition header has a value + * other than "attachment". + */ + Message message = (Message) body; + + // We add the Message object so we can extract the filename later. + viewables.add(new MessageHeader(part, message)); + + // Recurse to grab all viewable parts and attachments from that message. + viewables.addAll(getViewables(message, attachments)); + } else if (isPartTextualBody(part)) { + /* + * Save text/plain and text/html + */ + String mimeType = part.getMimeType(); + if (mimeType.equalsIgnoreCase("text/plain")) { + Text text = new Text(part); + viewables.add(text); + } else { + Html html = new Html(part); + viewables.add(html); + } + } else { + // Everything else is treated as attachment. + attachments.add(part); + } + + return viewables; + } + + public static Set getTextParts(Part part) throws MessagingException { + List attachments = new ArrayList(); + return getParts(getViewables(part, attachments)); + } + + /** + * Collect attachment parts of a message. + * @return A list of parts regarded as attachments. + * @throws MessagingException In case of an error. + */ + public static List collectAttachments(Message message) throws MessagingException { + try { + List attachments = new ArrayList(); + getViewables(message, attachments); + return attachments; + } catch (Exception e) { + throw new MessagingException("Couldn't collect attachment parts", e); + } + } + + /** + * Collect the viewable textual parts of a message. + * @return A set of viewable parts of the message. + * @throws MessagingException In case of an error. + */ + public static Set collectTextParts(Message message) throws MessagingException { + try { + return getTextParts(message); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } + + private static Message getMessageFromPart(Part part) { + while (part != null) { + if (part instanceof Message) + return (Message)part; + + if (!(part instanceof BodyPart)) + return null; + + Multipart multipart = ((BodyPart)part).getParent(); + if (multipart == null) + return null; + + part = multipart.getParent(); + } + return null; + } + + /** + * Search the children of a {@link Multipart} for {@code text/plain} parts. + * + * @param multipart The {@code Multipart} to search through. + * @param directChild If {@code true}, this method will return after the first {@code text/plain} was + * found. + * + * @return A list of {@link Text} viewables. + * + * @throws MessagingException + * In case of an error. + */ + private static List findTextPart(Multipart multipart, boolean directChild) + throws MessagingException { + List viewables = new ArrayList(); + + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + /* + * Recurse to find text parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/plain part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. multipart/mixed + * 1.1.1. text/plain + * 1.1.2. text/plain + * 1.2. text/html + */ + List textViewables = findTextPart(innerMultipart, false); + + if (!textViewables.isEmpty()) { + viewables.addAll(textViewables); + if (directChild) { + break; + } + } + } else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) { + Text text = new Text(part); + viewables.add(text); + if (directChild) { + break; + } + } + } + return viewables; + } + + /** + * Search the children of a {@link Multipart} for {@code text/html} parts. + * Every part that is not a {@code text/html} we want to display, we add to 'attachments'. + * + * @param multipart The {@code Multipart} to search through. + * @param knownTextParts A set of {@code text/plain} parts that shouldn't be added to 'attachments'. + * @param attachments A list that will receive the parts that are considered attachments. + * @param directChild If {@code true}, this method will add all {@code text/html} parts except the first + * found to 'attachments'. + * + * @return A list of {@link Text} viewables. + * + * @throws MessagingException In case of an error. + */ + private static List findHtmlPart(Multipart multipart, Set knownTextParts, + List attachments, boolean directChild) throws MessagingException { + List viewables = new ArrayList(); + + boolean partFound = false; + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + if (directChild && partFound) { + // We already found our text/html part. Now we're only looking for attachments. + findAttachments(innerMultipart, knownTextParts, attachments); + } else { + /* + * Recurse to find HTML parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/html part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. text/plain + * 1.2. multipart/mixed + * 1.2.1. text/html + * 1.2.2. text/html + * 1.3. image/jpeg + */ + List htmlViewables = findHtmlPart(innerMultipart, knownTextParts, + attachments, false); + + if (!htmlViewables.isEmpty()) { + partFound = true; + viewables.addAll(htmlViewables); + } + } + } else if (!(directChild && partFound) && isPartTextualBody(part) && + part.getMimeType().equalsIgnoreCase("text/html")) { + Html html = new Html(part); + viewables.add(html); + partFound = true; + } else if (!knownTextParts.contains(part)) { + // Only add this part as attachment if it's not a viewable text/plain part found + // earlier. + attachments.add(part); + } + } + + return viewables; + } + + /** + * Traverse the MIME tree and add everything that's not a known text part to 'attachments'. + * + * @param multipart + * The {@link Multipart} to start from. + * @param knownTextParts + * A set of known text parts we don't want to end up in 'attachments'. + * @param attachments + * A list that will receive the parts that are considered attachments. + */ + private static void findAttachments(Multipart multipart, Set knownTextParts, + List attachments) { + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + findAttachments(innerMultipart, knownTextParts, attachments); + } else if (!knownTextParts.contains(part)) { + attachments.add(part); + } + } + } + + /** + * Build a set of message parts for fast lookups. + * + * @param viewables + * A list of {@link Viewable}s containing references to the message parts to include in + * the set. + * + * @return The set of viewable {@code Part}s. + * + * @see MessageExtractor#findHtmlPart(Multipart, Set, List, boolean) + * @see MessageExtractor#findAttachments(Multipart, Set, List) + */ + private static Set getParts(List viewables) { + Set parts = new HashSet(); + + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + parts.add(((Textual) viewable).getPart()); + } else if (viewable instanceof Alternative) { + Alternative alternative = (Alternative) viewable; + parts.addAll(getParts(alternative.getText())); + parts.addAll(getParts(alternative.getHtml())); + } + } + + return parts; + } + + private static Boolean isPartTextualBody(Part part) throws MessagingException { + String disposition = part.getDisposition(); + String dispositionType = null; + String dispositionFilename = null; + if (disposition != null) { + dispositionType = MimeUtility.getHeaderParameter(disposition, null); + dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename"); + } + + /* + * A best guess that this part is intended to be an attachment and not inline. + */ + boolean attachment = ("attachment".equalsIgnoreCase(dispositionType) || (dispositionFilename != null)); + + if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) { + return true; + } + /* + * If the part is plain text and it got this far it's part of a + * mixed (et al) and should be rendered inline. + */ + else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) { + return true; + } + /* + * Finally, if it's nothing else we will include it as an attachment. + */ + else { + return false; + } + } + + private static String getContentDisposition(Part part) { + try { + String disposition = part.getDisposition(); + if (disposition != null) { + return MimeUtility.getHeaderParameter(disposition, null); + } + } catch (MessagingException e) { /* ignore */ } + return null; + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeHeader.java b/src/com/fsck/k9/mail/internet/MimeHeader.java index 12bd5fd31..7c9569df0 100644 --- a/src/com/fsck/k9/mail/internet/MimeHeader.java +++ b/src/com/fsck/k9/mail/internet/MimeHeader.java @@ -1,8 +1,6 @@ package com.fsck.k9.mail.internet; -import com.fsck.k9.helper.Utility; - import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; @@ -97,7 +95,7 @@ public class MimeHeader { public void writeTo(OutputStream out) throws IOException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); for (Field field : mFields) { - if (!Utility.arrayContains(writeOmitFields, field.name)) { + if (!Arrays.asList(writeOmitFields).contains(field.name)) { String v = field.value; if (hasToBeEncoded(v)) { diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 75fa217d8..eff4ffa2e 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -33,8 +33,6 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.store.UnavailableStorageException; -import com.fsck.k9.K9; /** * An implementation of Message that stores all of it's metadata in RFC 822 and @@ -138,12 +136,12 @@ public class MimeMessage extends Message { * @param sentDate * @throws com.fsck.k9.mail.MessagingException */ - public void addSentDate(Date sentDate) throws MessagingException { + public void addSentDate(Date sentDate, boolean hideTimeZone) throws MessagingException { if (mDateFormat == null) { mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); } - if (K9.hideTimeZone()) { + if (hideTimeZone) { mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } @@ -152,9 +150,9 @@ public class MimeMessage extends Message { } @Override - public void setSentDate(Date sentDate) throws MessagingException { + public void setSentDate(Date sentDate, boolean hideTimeZone) throws MessagingException { removeHeader("Date"); - addSentDate(sentDate); + addSentDate(sentDate, hideTimeZone); } public void setInternalSentDate(Date sentDate) { @@ -333,7 +331,7 @@ public class MimeMessage extends Message { return "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">"; } - public void setMessageId(String messageId) throws UnavailableStorageException { + public void setMessageId(String messageId) throws MessagingException { setHeader("Message-ID", messageId); mMessageId = messageId; } @@ -419,27 +417,27 @@ public class MimeMessage extends Message { } @Override - public void addHeader(String name, String value) throws UnavailableStorageException { + public void addHeader(String name, String value) throws MessagingException { mHeader.addHeader(name, value); } @Override - public void setHeader(String name, String value) throws UnavailableStorageException { + public void setHeader(String name, String value) throws MessagingException { mHeader.setHeader(name, value); } @Override - public String[] getHeader(String name) throws UnavailableStorageException { + public String[] getHeader(String name) throws MessagingException { return mHeader.getHeader(name); } @Override - public void removeHeader(String name) throws UnavailableStorageException { + public void removeHeader(String name) throws MessagingException { mHeader.removeHeader(name); } @Override - public Set getHeaderNames() throws UnavailableStorageException { + public Set getHeaderNames() throws MessagingException { return mHeader.getHeaderNames(); } @@ -474,7 +472,7 @@ public class MimeMessage extends Message { if (mBody instanceof Multipart) { ((Multipart)mBody).setCharset(charset); } else if (mBody instanceof TextBody) { - MimeUtility.setCharset(charset, this); + CharsetSupport.setCharset(charset, this); ((TextBody)mBody).setCharset(charset); } } @@ -608,28 +606,27 @@ public class MimeMessage extends Message { /** * Copy the contents of this object into another {@code MimeMessage} object. * - * @param message - * The {@code MimeMessage} object to receive the contents of this instance. + * @param destination The {@code MimeMessage} object to receive the contents of this instance. */ - protected void copy(MimeMessage message) { - super.copy(message); + protected void copy(MimeMessage destination) { + super.copy(destination); - message.mHeader = mHeader.clone(); + destination.mHeader = mHeader.clone(); - message.mBody = mBody; - message.mMessageId = mMessageId; - message.mSentDate = mSentDate; - message.mDateFormat = mDateFormat; - message.mSize = mSize; + destination.mBody = mBody; + destination.mMessageId = mMessageId; + destination.mSentDate = mSentDate; + destination.mDateFormat = mDateFormat; + destination.mSize = mSize; // These arrays are not supposed to be modified, so it's okay to reuse the references - message.mFrom = mFrom; - message.mTo = mTo; - message.mCc = mCc; - message.mBcc = mBcc; - message.mReplyTo = mReplyTo; - message.mReferences = mReferences; - message.mInReplyTo = mInReplyTo; + destination.mFrom = mFrom; + destination.mTo = mTo; + destination.mCc = mCc; + destination.mBcc = mBcc; + destination.mReplyTo = mReplyTo; + destination.mReferences = mReferences; + destination.mInReplyTo = mInReplyTo; } @Override diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index c6308a313..a5ac8efa7 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1,14 +1,12 @@ package com.fsck.k9.mail.internet; -import android.content.Context; -import android.util.Log; -import com.fsck.k9.K9; -import com.fsck.k9.R; -import com.fsck.k9.helper.HtmlConverter; -import com.fsck.k9.mail.*; -import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.internet.BinaryTempFileBody.BinaryTempFileBodyInputStream; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; @@ -19,31 +17,22 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.nio.charset.Charset; -import java.nio.charset.IllegalCharsetNameException; public class MimeUtility { public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; - public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; - private static final String TEXT_DIVIDER = - "------------------------------------------------------------------------"; - /* * http://www.w3schools.com/media/media_mimeref.asp * + * http://www.stdicon.com/mimetypes */ - public static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { + private static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { //* Do not delete the next two lines { "", DEFAULT_ATTACHMENT_MIME_TYPE }, { "k9s", K9_SETTINGS_MIME_TYPE}, @@ -896,29 +885,6 @@ public class MimeUtility { { "zmm", "application/vnd.handheld-entertainment+xml"} }; - /** - * Table for MIME type replacements. - * - * Table format: wrong type, correct type - */ - private static final String[][] MIME_TYPE_REPLACEMENT_MAP = new String[][] { - {"image/jpg", "image/jpeg"}, - {"image/pjpeg", "image/jpeg"}, // see issue 1712 - {"application/x-zip-compressed", "application/zip"} // see issue 3791 - }; - - /** - * Table for character set fall-back. - * - * Table format: unsupported charset (regular expression), fall-back charset - */ - private static final String[][] CHARSET_FALLBACK_MAP = new String[][] { - // Some Android versions don't support KOI8-U - {"koi8-u", "koi8-r"}, - {"iso-2022-jp-[\\d]+", "iso-2022-jp"}, - // Default fall-back is US-ASCII - {".*", "US-ASCII"} - }; public static String unfold(String s) { if (s == null) { @@ -930,9 +896,9 @@ public class MimeUtility { private static String decode(String s, Message message) { if (s == null) { return null; + } else { + return DecoderUtil.decodeEncodedWords(s, message); } - - return DecoderUtil.decodeEncodedWords(s, message); } public static String unfoldAndDecode(String s) { @@ -952,24 +918,24 @@ public class MimeUtility { * Returns the named parameter of a header field. If name is null the first * parameter is returned, or if there are no additional parameters in the * field the entire field is returned. Otherwise the named parameter is - * searched for in a case insensitive fashion and returned. If the parameter - * cannot be found the method returns null. + * searched for in a case insensitive fashion and returned. * - * @param header - * @param name - * @return + * @param headerValue the header value + * @param parameterName the parameter name + * @return the value. if the parameter cannot be found the method returns null. */ - public static String getHeaderParameter(String header, String name) { - if (header == null) { + public static String getHeaderParameter(String headerValue, String parameterName) { + if (headerValue == null) { return null; } - header = header.replaceAll("\r|\n", ""); - String[] parts = header.split(";"); - if (name == null && parts.length > 0) { + headerValue = headerValue.replaceAll("\r|\n", ""); + String[] parts = headerValue.split(";"); + if (parameterName == null && parts.length > 0) { return parts[0].trim(); } for (String part : parts) { - if (part.trim().toLowerCase(Locale.US).startsWith(name.toLowerCase(Locale.US))) { + if (parameterName != null && + part.trim().toLowerCase(Locale.US).startsWith(parameterName.toLowerCase(Locale.US))) { String[] partParts = part.split("=", 2); if (partParts.length == 2) { String parameter = partParts[1].trim(); @@ -985,12 +951,11 @@ public class MimeUtility { return null; } - public static Part findFirstPartByMimeType(Part part, String mimeType) - throws MessagingException { + public static Part findFirstPartByMimeType(Part part, String mimeType) throws MessagingException { if (part.getBody() instanceof Multipart) { Multipart multipart = (Multipart)part.getBody(); for (BodyPart bodyPart : multipart.getBodyParts()) { - Part ret = findFirstPartByMimeType(bodyPart, mimeType); + Part ret = MimeUtility.findFirstPartByMimeType(bodyPart, mimeType); if (ret != null) { return ret; } @@ -1001,104 +966,6 @@ public class MimeUtility { 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. - * @param part The part containing a body - * @return a String containing the converted text in the body, or null if there was no text - * or an error during conversion. - */ - public static String getTextFromPart(Part part) { - try { - if ((part != null) && (part.getBody() != null)) { - final Body body = part.getBody(); - if (body instanceof TextBody) { - return ((TextBody)body).getText(); - } - - final String mimeType = part.getMimeType(); - if ((mimeType != null) && MimeUtility.mimeTypeMatches(mimeType, "text/*")) { - /* - * We've got a text part, so let's see if it needs to be processed further. - */ - String charset = getHeaderParameter(part.getContentType(), "charset"); - /* - * determine the charset from HTML message. - */ - if (mimeType.equalsIgnoreCase("text/html") && charset == null) { - InputStream in = part.getBody().getInputStream(); - try { - byte[] buf = new byte[256]; - in.read(buf, 0, buf.length); - String str = new String(buf, "US-ASCII"); - - if (str.isEmpty()) { - return ""; - } - Pattern p = Pattern.compile("", Pattern.CASE_INSENSITIVE); - Matcher m = p.matcher(str); - if (m.find()) { - charset = m.group(1); - } - } finally { - try { - if (in instanceof BinaryTempFileBodyInputStream) { - /* - * If this is a BinaryTempFileBodyInputStream, calling close() - * will delete the file. But we can't let that happen because - * the file needs to be opened again by the code a few lines - * down. - */ - ((BinaryTempFileBodyInputStream) in).closeWithoutDeleting(); - } else { - in.close(); - } - } catch (Exception e) { /* ignore */ } - } - } - charset = fixupCharset(charset, getMessageFromPart(part)); - - /* - * Now we read the part into a buffer for further processing. Because - * the stream is now wrapped we'll remove any transfer encoding at this point. - */ - InputStream in = part.getBody().getInputStream(); - try { - String text = readToString(in, charset); - - // Replace the body with a TextBody that already contains the decoded text - part.setBody(new TextBody(text)); - - return text; - } finally { - try { - /* - * This time we don't care if it's a BinaryTempFileBodyInputStream. We - * replaced the body with a TextBody instance and hence don't need the - * file anymore. - */ - in.close(); - } catch (IOException e) { /* Ignore */ } - } - } - } - - } catch (OutOfMemoryError oom) { - /* - * If we are not able to process the body there's nothing we can do about it. Return - * null and let the upper layers handle the missing content. - */ - Log.e(K9.LOG_TAG, "Unable to getTextFromPart " + oom.toString()); - } catch (Exception e) { - /* - * If we are not able to process the body there's nothing we can do about it. Return - * null and let the upper layers handle the missing content. - */ - Log.e(K9.LOG_TAG, "Unable to getTextFromPart", e); - } - return null; - } - /** * Returns true if the given mimeType matches the matchAgainst specification. * @param mimeType A MIME type to check. @@ -1127,7 +994,7 @@ public class MimeUtility { */ if (contentTransferEncoding != null) { contentTransferEncoding = - MimeUtility.getHeaderParameter(contentTransferEncoding, null); + getHeaderParameter(contentTransferEncoding, null); if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(contentTransferEncoding)) { in = new QuotedPrintableInputStream(in); } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(contentTransferEncoding)) { @@ -1151,900 +1018,6 @@ public class MimeUtility { return tempBody; } - - /** - * Empty base class for the class hierarchy used by - * {@link MimeUtility#extractTextAndAttachments(Context, Message)}. - * - * @see Text - * @see Html - * @see MessageHeader - * @see Alternative - */ - static abstract class Viewable { /* empty */ } - - /** - * Class representing textual parts of a message that aren't marked as attachments. - * - * @see MimeUtility#isPartTextualBody(Part) - */ - static abstract class Textual extends Viewable { - private Part mPart; - - public Textual(Part part) { - mPart = part; - } - - public Part getPart() { - return mPart; - } - } - - /** - * Class representing a {@code text/plain} part of a message. - */ - static class Text extends Textual { - public Text(Part part) { - super(part); - } - } - - /** - * Class representing a {@code text/html} part of a message. - */ - static class Html extends Textual { - public Html(Part part) { - super(part); - } - } - - /** - * Class representing a {@code message/rfc822} part of a message. - * - *

    - * This is used to extract basic header information when the message contents are displayed - * inline. - *

    - */ - static class MessageHeader extends Viewable { - private Part mContainerPart; - private Message mMessage; - - public MessageHeader(Part containerPart, Message message) { - mContainerPart = containerPart; - mMessage = message; - } - - public Part getContainerPart() { - return mContainerPart; - } - - public Message getMessage() { - return mMessage; - } - } - - /** - * Class representing a {@code multipart/alternative} part of a message. - * - *

    - * Only relevant {@code text/plain} and {@code text/html} children are stored in this container - * class. - *

    - */ - static class Alternative extends Viewable { - private List mText; - private List mHtml; - - public Alternative(List text, List html) { - mText = text; - mHtml = html; - } - - public List getText() { - return mText; - } - - public List getHtml() { - return mHtml; - } - } - - /** - * Store viewable text of a message as plain text and HTML, and the parts considered - * attachments. - * - * @see MimeUtility#extractTextAndAttachments(Context, Message) - */ - public static class ViewableContainer { - /** - * The viewable text of the message in plain text. - */ - public final String text; - - /** - * The viewable text of the message in HTML. - */ - public final String html; - - /** - * The parts of the message considered attachments (everything not viewable). - */ - public final List attachments; - - ViewableContainer(String text, String html, List attachments) { - this.text = text; - this.html = html; - this.attachments = attachments; - } - } - - /** - * Collect attachment parts of a message. - * - * @param message - * The message to collect the attachment parts from. - * - * @return A list of parts regarded as attachments. - * - * @throws MessagingException - * In case of an error. - */ - public static List collectAttachments(Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - getViewables(message, attachments); - - return attachments; - } catch (Exception e) { - throw new MessagingException("Couldn't collect attachment parts", e); - } - } - - /** - * Collect the viewable textual parts of a message. - * - * @param message - * The message to extract the viewable parts from. - * - * @return A set of viewable parts of the message. - * - * @throws MessagingException - * In case of an error. - */ - public static Set collectTextParts(Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - - // Collect all viewable parts - List viewables = getViewables(message, attachments); - - // Extract the Part references - return getParts(viewables); - } catch (Exception e) { - throw new MessagingException("Couldn't extract viewable parts", e); - } - } - - /** - * Extract the viewable textual parts of a message and return the rest as attachments. - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param message - * The message to extract the text and attachments from. - * - * @return A {@link ViewableContainer} instance containing the textual parts of the message as - * plain text and HTML, and a list of message parts considered attachments. - * - * @throws MessagingException - * In case of an error. - */ - public static ViewableContainer extractTextAndAttachments(Context context, Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - - // Collect all viewable parts - List viewables = getViewables(message, attachments); - - /* - * Convert the tree of viewable parts into text and HTML - */ - - // Used to suppress the divider for the first viewable part - boolean hideDivider = true; - - StringBuilder text = new StringBuilder(); - StringBuilder html = new StringBuilder(); - - for (Viewable viewable : viewables) { - if (viewable instanceof Textual) { - // This is either a text/plain or text/html part. Fill the variables 'text' and - // 'html', converting between plain text and HTML as necessary. - text.append(buildText(viewable, !hideDivider)); - html.append(buildHtml(viewable, !hideDivider)); - hideDivider = false; - } else if (viewable instanceof MessageHeader) { - MessageHeader header = (MessageHeader) viewable; - Part containerPart = header.getContainerPart(); - Message innerMessage = header.getMessage(); - - addTextDivider(text, containerPart, !hideDivider); - addMessageHeaderText(context, text, innerMessage); - - addHtmlDivider(html, containerPart, !hideDivider); - addMessageHeaderHtml(context, html, innerMessage); - - hideDivider = true; - } else if (viewable instanceof Alternative) { - // Handle multipart/alternative contents - Alternative alternative = (Alternative) viewable; - - /* - * We made sure at least one of text/plain or text/html is present when - * creating the Alternative object. If one part is not present we convert the - * other one to make sure 'text' and 'html' always contain the same text. - */ - List textAlternative = alternative.getText().isEmpty() ? - alternative.getHtml() : alternative.getText(); - List htmlAlternative = alternative.getHtml().isEmpty() ? - alternative.getText() : alternative.getHtml(); - - // Fill the 'text' variable - boolean divider = !hideDivider; - for (Viewable textViewable : textAlternative) { - text.append(buildText(textViewable, divider)); - divider = true; - } - - // Fill the 'html' variable - divider = !hideDivider; - for (Viewable htmlViewable : htmlAlternative) { - html.append(buildHtml(htmlViewable, divider)); - divider = true; - } - hideDivider = false; - } - } - - return new ViewableContainer(text.toString(), html.toString(), attachments); - } catch (Exception e) { - throw new MessagingException("Couldn't extract viewable parts", e); - } - } - - /** - * Traverse the MIME tree of a message an extract viewable parts. - * - * @param part - * The message part to start from. - * @param attachments - * A list that will receive the parts that are considered attachments. - * - * @return A list of {@link Viewable}s. - * - * @throws MessagingException - * In case of an error. - */ - private static List getViewables(Part part, List attachments) throws MessagingException { - List viewables = new ArrayList(); - - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart multipart = (Multipart) body; - if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) { - /* - * For multipart/alternative parts we try to find a text/plain and a text/html - * child. Everything else we find is put into 'attachments'. - */ - List text = findTextPart(multipart, true); - - Set knownTextParts = getParts(text); - List html = findHtmlPart(multipart, knownTextParts, attachments, true); - - if (!text.isEmpty() || !html.isEmpty()) { - Alternative alternative = new Alternative(text, html); - viewables.add(alternative); - } - } else { - // For all other multipart parts we recurse to grab all viewable children. - for (Part bodyPart : multipart.getBodyParts()) { - viewables.addAll(getViewables(bodyPart, attachments)); - } - } - } else if (body instanceof Message && - !("attachment".equalsIgnoreCase(getContentDisposition(part)))) { - /* - * We only care about message/rfc822 parts whose Content-Disposition header has a value - * other than "attachment". - */ - Message message = (Message) body; - - // We add the Message object so we can extract the filename later. - viewables.add(new MessageHeader(part, message)); - - // Recurse to grab all viewable parts and attachments from that message. - viewables.addAll(getViewables(message, attachments)); - } else if (isPartTextualBody(part)) { - /* - * Save text/plain and text/html - */ - String mimeType = part.getMimeType(); - if (mimeType.equalsIgnoreCase("text/plain")) { - Text text = new Text(part); - viewables.add(text); - } else { - Html html = new Html(part); - viewables.add(html); - } - } else { - // Everything else is treated as attachment. - attachments.add(part); - } - - return viewables; - } - - /** - * Search the children of a {@link Multipart} for {@code text/plain} parts. - * - * @param multipart - * The {@code Multipart} to search through. - * @param directChild - * If {@code true}, this method will return after the first {@code text/plain} was - * found. - * - * @return A list of {@link Text} viewables. - * - * @throws MessagingException - * In case of an error. - */ - private static List findTextPart(Multipart multipart, boolean directChild) - throws MessagingException { - List viewables = new ArrayList(); - - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - - /* - * Recurse to find text parts. Since this is a multipart that is a child of a - * multipart/alternative we don't want to stop after the first text/plain part - * we find. This will allow to get all text parts for constructions like this: - * - * 1. multipart/alternative - * 1.1. multipart/mixed - * 1.1.1. text/plain - * 1.1.2. text/plain - * 1.2. text/html - */ - List textViewables = findTextPart(innerMultipart, false); - - if (!textViewables.isEmpty()) { - viewables.addAll(textViewables); - if (directChild) { - break; - } - } - } else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) { - Text text = new Text(part); - viewables.add(text); - if (directChild) { - break; - } - } - } - - return viewables; - } - - /** - * Search the children of a {@link Multipart} for {@code text/html} parts. - * - *

    - * Every part that is not a {@code text/html} we want to display, we add to 'attachments'. - *

    - * - * @param multipart - * The {@code Multipart} to search through. - * @param knownTextParts - * A set of {@code text/plain} parts that shouldn't be added to 'attachments'. - * @param attachments - * A list that will receive the parts that are considered attachments. - * @param directChild - * If {@code true}, this method will add all {@code text/html} parts except the first - * found to 'attachments'. - * - * @return A list of {@link Text} viewables. - * - * @throws MessagingException - * In case of an error. - */ - private static List findHtmlPart(Multipart multipart, Set knownTextParts, - List attachments, boolean directChild) throws MessagingException { - List viewables = new ArrayList(); - - boolean partFound = false; - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - - if (directChild && partFound) { - // We already found our text/html part. Now we're only looking for attachments. - findAttachments(innerMultipart, knownTextParts, attachments); - } else { - /* - * Recurse to find HTML parts. Since this is a multipart that is a child of a - * multipart/alternative we don't want to stop after the first text/html part - * we find. This will allow to get all text parts for constructions like this: - * - * 1. multipart/alternative - * 1.1. text/plain - * 1.2. multipart/mixed - * 1.2.1. text/html - * 1.2.2. text/html - * 1.3. image/jpeg - */ - List htmlViewables = findHtmlPart(innerMultipart, knownTextParts, - attachments, false); - - if (!htmlViewables.isEmpty()) { - partFound = true; - viewables.addAll(htmlViewables); - } - } - } else if (!(directChild && partFound) && isPartTextualBody(part) && - part.getMimeType().equalsIgnoreCase("text/html")) { - Html html = new Html(part); - viewables.add(html); - partFound = true; - } else if (!knownTextParts.contains(part)) { - // Only add this part as attachment if it's not a viewable text/plain part found - // earlier. - attachments.add(part); - } - } - - return viewables; - } - - /** - * Build a set of message parts for fast lookups. - * - * @param viewables - * A list of {@link Viewable}s containing references to the message parts to include in - * the set. - * - * @return The set of viewable {@code Part}s. - * - * @see MimeUtility#findHtmlPart(Multipart, Set, List, boolean) - * @see MimeUtility#findAttachments(Multipart, Set, List) - */ - private static Set getParts(List viewables) { - Set parts = new HashSet(); - - for (Viewable viewable : viewables) { - if (viewable instanceof Textual) { - parts.add(((Textual) viewable).getPart()); - } else if (viewable instanceof Alternative) { - Alternative alternative = (Alternative) viewable; - parts.addAll(getParts(alternative.getText())); - parts.addAll(getParts(alternative.getHtml())); - } - } - - return parts; - } - - /** - * Traverse the MIME tree and add everything that's not a known text part to 'attachments'. - * - * @param multipart - * The {@link Multipart} to start from. - * @param knownTextParts - * A set of known text parts we don't want to end up in 'attachments'. - * @param attachments - * A list that will receive the parts that are considered attachments. - */ - private static void findAttachments(Multipart multipart, Set knownTextParts, - List attachments) { - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - findAttachments(innerMultipart, knownTextParts, attachments); - } else if (!knownTextParts.contains(part)) { - attachments.add(part); - } - } - } - - /** - * Extract important header values from a message to display inline (plain text version). - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param text - * The {@link StringBuilder} that will receive the (plain text) output. - * @param message - * The message to extract the header values from. - * - * @throws MessagingException - * In case of an error. - */ - private static void addMessageHeaderText(Context context, StringBuilder text, Message message) - throws MessagingException { - // From: - Address[] from = message.getFrom(); - if (from != null && from.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_from)); - text.append(' '); - text.append(Address.toString(from)); - text.append("\r\n"); - } - - // To: - Address[] to = message.getRecipients(RecipientType.TO); - if (to != null && to.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_to)); - text.append(' '); - text.append(Address.toString(to)); - text.append("\r\n"); - } - - // Cc: - Address[] cc = message.getRecipients(RecipientType.CC); - if (cc != null && cc.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_cc)); - text.append(' '); - text.append(Address.toString(cc)); - text.append("\r\n"); - } - - // Date: - Date date = message.getSentDate(); - if (date != null) { - text.append(context.getString(R.string.message_compose_quote_header_send_date)); - text.append(' '); - text.append(date.toString()); - text.append("\r\n"); - } - - // Subject: - String subject = message.getSubject(); - text.append(context.getString(R.string.message_compose_quote_header_subject)); - text.append(' '); - if (subject == null) { - text.append(context.getString(R.string.general_no_subject)); - } else { - text.append(subject); - } - text.append("\r\n\r\n"); - } - - /** - * Extract important header values from a message to display inline (HTML version). - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param html - * The {@link StringBuilder} that will receive the (HTML) output. - * @param message - * The message to extract the header values from. - * - * @throws MessagingException - * In case of an error. - */ - private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message) - throws MessagingException { - - html.append(""); - - // From: - Address[] from = message.getFrom(); - if (from != null && from.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_from), - Address.toString(from)); - } - - // To: - Address[] to = message.getRecipients(RecipientType.TO); - if (to != null && to.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_to), - Address.toString(to)); - } - - // Cc: - Address[] cc = message.getRecipients(RecipientType.CC); - if (cc != null && cc.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_cc), - Address.toString(cc)); - } - - // Date: - Date date = message.getSentDate(); - if (date != null) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date), - date.toString()); - } - - // Subject: - String subject = message.getSubject(); - addTableRow(html, context.getString(R.string.message_compose_quote_header_subject), - (subject == null) ? context.getString(R.string.general_no_subject) : subject); - - html.append("
    "); - } - - /** - * Output an HTML table two column row with some hardcoded style. - * - * @param html - * The {@link StringBuilder} that will receive the output. - * @param header - * The string to be put in the {@code TH} element. - * @param value - * The string to be put in the {@code TD} element. - */ - private static void addTableRow(StringBuilder html, String header, String value) { - html.append(""); - html.append(header); - html.append(""); - html.append(""); - html.append(value); - html.append(""); - } - - /** - * Use the contents of a {@link Viewable} to create the plain text to be displayed. - * - *

    - * This will use {@link HtmlConverter#htmlToText(String)} to convert HTML parts to plain text - * if necessary. - *

    - * - * @param viewable - * The viewable part to build the text from. - * @param prependDivider - * {@code true}, if the text divider should be inserted as first element. - * {@code false}, otherwise. - * - * @return The contents of the supplied viewable instance as plain text. - */ - private static StringBuilder buildText(Viewable viewable, boolean prependDivider) - { - StringBuilder text = new StringBuilder(); - if (viewable instanceof Textual) { - Part part = ((Textual)viewable).getPart(); - addTextDivider(text, part, prependDivider); - - String t = getTextFromPart(part); - if (t == null) { - t = ""; - } else if (viewable instanceof Html) { - t = HtmlConverter.htmlToText(t); - } - text.append(t); - } else if (viewable instanceof Alternative) { - // That's odd - an Alternative as child of an Alternative; go ahead and try to use the - // text/plain child; fall-back to the text/html part. - Alternative alternative = (Alternative) viewable; - - List textAlternative = alternative.getText().isEmpty() ? - alternative.getHtml() : alternative.getText(); - - boolean divider = prependDivider; - for (Viewable textViewable : textAlternative) { - text.append(buildText(textViewable, divider)); - divider = true; - } - } - - return text; - } - - /* - * Some constants that are used by addTextDivider() below. - */ - private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); - private static final String FILENAME_PREFIX = "----- "; - private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); - private static final String FILENAME_SUFFIX = " "; - private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); - - /** - * Add a plain text divider between two plain text message parts. - * - * @param text - * The {@link StringBuilder} to append the divider to. - * @param part - * The message part that will follow after the divider. This is used to extract the - * part's name. - * @param prependDivider - * {@code true}, if the divider should be appended. {@code false}, otherwise. - */ - private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) { - if (prependDivider) { - String filename = getPartName(part); - - text.append("\r\n\r\n"); - int len = filename.length(); - if (len > 0) { - if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) { - filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - - FILENAME_SUFFIX_LENGTH - 3) + "..."; - } - text.append(FILENAME_PREFIX); - text.append(filename); - text.append(FILENAME_SUFFIX); - text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - - FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); - } else { - text.append(TEXT_DIVIDER); - } - text.append("\r\n\r\n"); - } - } - - /** - * Use the contents of a {@link Viewable} to create the HTML to be displayed. - * - *

    - * This will use {@link HtmlConverter#textToHtml(String)} to convert plain text parts - * to HTML if necessary. - *

    - * - * @param viewable - * The viewable part to build the HTML from. - * @param prependDivider - * {@code true}, if the HTML divider should be inserted as first element. - * {@code false}, otherwise. - * - * @return The contents of the supplied viewable instance as HTML. - */ - private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider) - { - StringBuilder html = new StringBuilder(); - if (viewable instanceof Textual) { - Part part = ((Textual)viewable).getPart(); - addHtmlDivider(html, part, prependDivider); - - String t = getTextFromPart(part); - if (t == null) { - t = ""; - } else if (viewable instanceof Text) { - t = HtmlConverter.textToHtml(t); - } - html.append(t); - } else if (viewable instanceof Alternative) { - // That's odd - an Alternative as child of an Alternative; go ahead and try to use the - // text/html child; fall-back to the text/plain part. - Alternative alternative = (Alternative) viewable; - - List htmlAlternative = alternative.getHtml().isEmpty() ? - alternative.getText() : alternative.getHtml(); - - boolean divider = prependDivider; - for (Viewable htmlViewable : htmlAlternative) { - html.append(buildHtml(htmlViewable, divider)); - divider = true; - } - } - - return html; - } - - /** - * Add an HTML divider between two HTML message parts. - * - * @param html - * The {@link StringBuilder} to append the divider to. - * @param part - * The message part that will follow after the divider. This is used to extract the - * part's name. - * @param prependDivider - * {@code true}, if the divider should be appended. {@code false}, otherwise. - */ - private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) { - if (prependDivider) { - String filename = getPartName(part); - - html.append("

    "); - html.append(filename); - html.append("

    "); - } - } - - /** - * Get the name of the message part. - * - * @param part - * The part to get the name for. - * - * @return The (file)name of the part if available. An empty string, otherwise. - */ - private static String getPartName(Part part) { - try { - String disposition = part.getDisposition(); - if (disposition != null) { - String name = MimeUtility.getHeaderParameter(disposition, "filename"); - return (name == null) ? "" : name; - } - } - catch (MessagingException e) { /* ignore */ } - - return ""; - } - - /** - * Get the value of the {@code Content-Disposition} header. - * - * @param part - * The message part to read the header from. - * - * @return The value of the {@code Content-Disposition} header if available. {@code null}, - * otherwise. - */ - private static String getContentDisposition(Part part) { - try { - String disposition = part.getDisposition(); - if (disposition != null) { - return MimeUtility.getHeaderParameter(disposition, null); - } - } - catch (MessagingException e) { /* ignore */ } - - return null; - } - - private static Boolean isPartTextualBody(Part part) throws MessagingException { - String disposition = part.getDisposition(); - String dispositionType = null; - String dispositionFilename = null; - if (disposition != null) { - dispositionType = MimeUtility.getHeaderParameter(disposition, null); - dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename"); - } - - /* - * A best guess that this part is intended to be an attachment and not inline. - */ - boolean attachment = ("attachment".equalsIgnoreCase(dispositionType) || (dispositionFilename != null)); - - if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) { - return true; - } - /* - * If the part is plain text and it got this far it's part of a - * mixed (et al) and should be rendered inline. - */ - else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) { - return true; - } - /* - * Finally, if it's nothing else we will include it as an attachment. - */ - else { - return false; - } - } - - public static String getCharsetFromAddress(String address) { - String variant = getJisVariantFromAddress(address); - if (variant != null) { - String charset = "x-" + variant + "-shift_jis-2007"; - if (Charset.isSupported(charset)) - return charset; - } - - return "UTF-8"; - } - public static String getMimeTypeByExtension(String filename) { String returnedType = null; String extension = null; @@ -2079,50 +1052,6 @@ public class MimeUtility { return null; } - /** - * Convert some wrong MIME types encountered in the wild to canonical MIME types. - * - * @param mimeType - * The original MIME type - * - * @return If {@code mimeType} is known to be wrong the correct MIME type is returned. - * Otherwise the lower case version of {@code mimeType} is returned. - * - * @see #MIME_TYPE_REPLACEMENT_MAP - */ - private static String canonicalizeMimeType(String mimeType) { - String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); - for (String[] mimeTypeMapEntry : MIME_TYPE_REPLACEMENT_MAP) { - if (mimeTypeMapEntry[0].equals(lowerCaseMimeType)) { - return mimeTypeMapEntry[1]; - } - } - return lowerCaseMimeType; - } - - /** - * When viewing the attachment we want the MIME type to be as sensible as possible. So we fix - * it up if necessary. - * - * @param mimeType - * The original MIME type of the attachment. - * @param name - * The (file)name of the attachment. - * - * @return The best MIME type we can come up with. - */ - public static String getMimeTypeForViewing(String mimeType, String name) { - if (DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType)) { - // If the MIME type is the generic "application/octet-stream" - // we try to find a better one by looking at the file extension. - return getMimeTypeByExtension(name); - } - - // Some messages contain wrong MIME types. See if we know better. - return canonicalizeMimeType(mimeType); - } - - /** * Get a default content-transfer-encoding for use with a given content-type * when adding an unencoded attachment. It's possible that 8bit encodings @@ -2153,1240 +1082,4 @@ public class MimeUtility { return (MimeUtil.ENC_BASE64); } } - - private static Message getMessageFromPart(Part part) { - while (part != null) { - if (part instanceof Message) - return (Message)part; - - if (!(part instanceof BodyPart)) - return null; - - Multipart multipart = ((BodyPart)part).getParent(); - if (multipart == null) - return null; - - part = multipart.getParent(); - } - return null; - } - - public static String fixupCharset(String charset, Message message) throws MessagingException { - if (charset == null || "0".equals(charset)) - charset = "US-ASCII"; // No encoding, so use us-ascii, which is the standard. - - charset = charset.toLowerCase(Locale.US); - if (charset.equals("cp932")) - charset = "shift_jis"; - - if (charset.equals("shift_jis") || charset.equals("iso-2022-jp")) { - String variant = getJisVariantFromMessage(message); - if (variant != null) - charset = "x-" + variant + "-" + charset + "-2007"; - } - return charset; - } - - private static String getJisVariantFromMessage(Message message) throws MessagingException { - if (message == null) - return null; - - // If a receiver is known to use a JIS variant, the sender transfers the message after converting the - // charset as a convention. - String variant = getJisVariantFromReceivedHeaders(message); - if (variant != null) - return variant; - - // If a receiver is not known to use any JIS variants, the sender transfers the message without converting - // the charset. - variant = getJisVariantFromFromHeaders(message); - if (variant != null) - return variant; - - return getJisVariantFromMailerHeaders(message); - } - - private static String getJisVariantFromReceivedHeaders(Message message) throws MessagingException { - String receivedHeaders[] = message.getHeader("Received"); - if (receivedHeaders == null) - return null; - - for (String receivedHeader : receivedHeaders) { - String address = getAddressFromReceivedHeader(receivedHeader); - if (address == null) - continue; - String variant = getJisVariantFromAddress(address); - if (variant != null) - return variant; - } - return null; - } - - private static String getAddressFromReceivedHeader(String receivedHeader) { - // Not implemented yet! Extract an address from the FOR clause of the given Received header. - return null; - } - - private static String getJisVariantFromFromHeaders(Message message) throws MessagingException { - Address addresses[] = message.getFrom(); - if (addresses == null || addresses.length == 0) - return null; - - return getJisVariantFromAddress(addresses[0].getAddress()); - } - - private static String getJisVariantFromAddress(String address) { - if (address == null) - return null; - if (isInDomain(address, "docomo.ne.jp") || isInDomain(address, "dwmail.jp") || - isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com") || - isInDomain(address, "emnet.ne.jp") || isInDomain(address, "emobile.ne.jp")) - return "docomo"; - else if (isInDomain(address, "softbank.ne.jp") || isInDomain(address, "vodafone.ne.jp") || - isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) - return "softbank"; - else if (isInDomain(address, "ezweb.ne.jp") || isInDomain(address, "ido.ne.jp")) - return "kddi"; - return null; - } - - private static boolean isInDomain(String address, String domain) { - int index = address.length() - domain.length() - 1; - if (index < 0) - return false; - - char c = address.charAt(index); - if (c != '@' && c != '.') - return false; - - return address.endsWith(domain); - } - - private static String getJisVariantFromMailerHeaders(Message message) throws MessagingException { - String mailerHeaders[] = message.getHeader("X-Mailer"); - if (mailerHeaders == null || mailerHeaders.length == 0) - return null; - - if (mailerHeaders[0].startsWith("iPhone Mail ") || mailerHeaders[0].startsWith("iPad Mail ")) - return "iphone"; - - return null; - } - - public static String readToString(InputStream in, String charset) throws IOException { - boolean isIphoneString = false; - - // iso-2022-jp variants are supported by no versions as of Dec 2010. - if (charset.length() > 19 && charset.startsWith("x-") && - charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { - in = new Iso2022JpToShiftJisInputStream(in); - charset = "x-" + charset.substring(2, charset.length() - 17) + "-shift_jis-2007"; - } - - // shift_jis variants are supported by Eclair and later. - if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007") && !Charset.isSupported(charset)) { - // If the JIS variant is iPhone, map the Unicode private use area in iPhone to the one in Android after - // converting the character set from the standard Shift JIS to Unicode. - if (charset.substring(2, charset.length() - 15).equals("iphone")) - isIphoneString = true; - - charset = "shift_jis"; - } - - /* - * See if there is conversion from the MIME charset to the Java one. - * this function may also throw an exception if the charset name is not known - */ - boolean supported; - try { - supported = Charset.isSupported(charset); - } catch (IllegalCharsetNameException e) { - supported = false; - } - - for (String[] rule: CHARSET_FALLBACK_MAP) { - if (supported) { - break; - } - - if (charset.matches(rule[0])) { - Log.e(K9.LOG_TAG, "I don't know how to deal with the charset " + charset + - ". Falling back to " + rule[1]); - charset = rule[1]; - try { - supported = Charset.isSupported(charset); - } catch (IllegalCharsetNameException e) { - supported = false; - } - } - } - - /* - * Convert and return as new String - */ - String str = IOUtils.toString(in, charset); - - if (isIphoneString) - str = importStringFromIphone(str); - return str; - } - - static private String importStringFromIphone(String str) { - StringBuilder buff = new StringBuilder(str.length()); - for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) { - int codePoint = str.codePointAt(i); - buff.appendCodePoint(importCodePointFromIphone(codePoint)); - } - return buff.toString(); - } - - static private int importCodePointFromIphone(int codePoint) { - switch (codePoint) { - case 0xE001: - return 0xFE19B; - case 0xE002: - return 0xFE19C; - case 0xE003: - return 0xFE823; - case 0xE004: - return 0xFE19D; - case 0xE005: - return 0xFE19E; - case 0xE006: - return 0xFE4CF; - case 0xE007: - return 0xFE4CD; - case 0xE008: - return 0xFE4EF; - case 0xE009: - return 0xFE523; - case 0xE00A: - return 0xFE525; - case 0xE00B: - return 0xFE528; - case 0xE00C: - return 0xFE538; - case 0xE00D: - return 0xFEB96; - case 0xE00E: - return 0xFEB97; - case 0xE00F: - return 0xFEB98; - case 0xE010: - return 0xFEB93; - case 0xE011: - return 0xFEB94; - case 0xE012: - return 0xFEB95; - case 0xE013: - return 0xFE7D5; - case 0xE014: - return 0xFE7D2; - case 0xE015: - return 0xFE7D3; - case 0xE016: - return 0xFE7D1; - case 0xE017: - return 0xFE7DA; - case 0xE018: - return 0xFE7D4; - case 0xE019: - return 0xFE1BD; - case 0xE01A: - return 0xFE1BE; - case 0xE01B: - return 0xFE7E4; - case 0xE01C: - return 0xFE7EA; - case 0xE01D: - return 0xFE7E9; - case 0xE01E: - return 0xFE7DF; - case 0xE01F: - return 0xFE7E3; - case 0xE020: - return 0xFEB09; - case 0xE021: - return 0xFEB04; - case 0xE022: - return 0xFEB0C; - case 0xE023: - return 0xFEB0E; - case 0xE024: - return 0xFE01E; - case 0xE025: - return 0xFE01F; - case 0xE026: - return 0xFE020; - case 0xE027: - return 0xFE021; - case 0xE028: - return 0xFE022; - case 0xE029: - return 0xFE023; - case 0xE02A: - return 0xFE024; - case 0xE02B: - return 0xFE025; - case 0xE02C: - return 0xFE026; - case 0xE02D: - return 0xFE027; - case 0xE02E: - return 0xFE028; - case 0xE02F: - return 0xFE029; - case 0xE030: - return 0xFE040; - case 0xE031: - return 0xFE4D2; - case 0xE032: - return 0xFE041; - case 0xE033: - return 0xFE512; - case 0xE034: - return 0xFE825; - case 0xE035: - return 0xFE826; - case 0xE036: - return 0xFE4B0; - case 0xE037: - return 0xFE4BB; - case 0xE038: - return 0xFE4B2; - case 0xE039: - return 0xFE7EC; - case 0xE03A: - return 0xFE7F5; - case 0xE03B: - return 0xFE4C3; - case 0xE03C: - return 0xFE800; - case 0xE03D: - return 0xFE801; - case 0xE03E: - return 0xFE813; - case 0xE03F: - return 0xFEB82; - case 0xE040: - return 0xFE815; - case 0xE041: - return 0xFE816; - case 0xE042: - return 0xFE818; - case 0xE043: - return 0xFE980; - case 0xE044: - return 0xFE982; - case 0xE045: - return 0xFE981; - case 0xE046: - return 0xFE962; - case 0xE047: - return 0xFE983; - case 0xE048: - return 0xFE003; - case 0xE049: - return 0xFE001; - case 0xE04A: - return 0xFE000; - case 0xE04B: - return 0xFE002; - case 0xE04C: - return 0xFE014; - case 0xE04D: - return 0xFE009; - case 0xE04E: - return 0xFE1AF; - case 0xE04F: - return 0xFE1B8; - case 0xE050: - return 0xFE1C0; - case 0xE051: - return 0xFE1C1; - case 0xE052: - return 0xFE1B7; - case 0xE053: - return 0xFE1C2; - case 0xE054: - return 0xFE1C3; - case 0xE055: - return 0xFE1BC; - case 0xE056: - return 0xFE335; - case 0xE057: - return 0xFE330; - case 0xE058: - return 0xFE323; - case 0xE059: - return 0xFE320; - case 0xE05A: - return 0xFE4F4; - case 0xE101: - return 0xFE52D; - case 0xE102: - return 0xFE52E; - case 0xE103: - return 0xFE52B; - case 0xE104: - return 0xFE526; - case 0xE105: - return 0xFE329; - case 0xE106: - return 0xFE327; - case 0xE107: - return 0xFE341; - case 0xE108: - return 0xFE344; - case 0xE109: - return 0xFE1C4; - case 0xE10A: - return 0xFE1C5; - case 0xE10B: - return 0xFE1BF; - case 0xE10C: - return 0xFE1B0; - case 0xE10D: - return 0xFE7ED; - case 0xE10E: - return 0xFE4D1; - case 0xE10F: - return 0xFEB56; - case 0xE110: - return 0xFE03C; - case 0xE111: - return 0xFE827; - case 0xE112: - return 0xFE510; - case 0xE113: - return 0xFE4F5; - case 0xE114: - return 0xFEB85; - case 0xE115: - return 0xFE7D9; - case 0xE116: - return 0xFE4CA; - case 0xE117: - return 0xFE515; - case 0xE118: - return 0xFE03F; - case 0xE119: - return 0xFE042; - case 0xE11A: - return 0xFE1B2; - case 0xE11B: - return 0xFE1AE; - case 0xE11C: - return 0xFE1B3; - case 0xE11D: - return 0xFE4F6; - case 0xE11E: - return 0xFE53B; - case 0xE11F: - return 0xFE537; - case 0xE120: - return 0xFE960; - case 0xE121: - return 0xFE4BC; - case 0xE122: - return 0xFE7FB; - case 0xE123: - return 0xFE7FA; - case 0xE124: - return 0xFE7FD; - case 0xE125: - return 0xFE807; - case 0xE126: - return 0xFE81D; - case 0xE127: - return 0xFE81E; - case 0xE128: - return 0xFE81F; - case 0xE129: - return 0xFE820; - case 0xE12A: - return 0xFE81C; - case 0xE12B: - return 0xFE1B1; - case 0xE12C: - return 0xFE81B; - case 0xE12D: - return 0xFE80B; - case 0xE12E: - return 0xFEB32; - case 0xE12F: - return 0xFE4DD; - case 0xE130: - return 0xFE80C; - case 0xE131: - return 0xFE7DB; - case 0xE132: - return 0xFE7D7; - case 0xE133: - return 0xFE80D; - case 0xE134: - return 0xFE7DC; - case 0xE135: - return 0xFE7EE; - case 0xE136: - return 0xFE7EB; - case 0xE137: - return 0xFE7F8; - case 0xE138: - return 0xFEB33; - case 0xE139: - return 0xFEB34; - case 0xE13A: - return 0xFEB35; - case 0xE13B: - return 0xFE509; - case 0xE13C: - return 0xFEB59; - case 0xE13D: - return 0xFE004; - case 0xE13E: - return 0xFE4D6; - case 0xE13F: - return 0xFE505; - case 0xE140: - return 0xFE507; - case 0xE141: - return 0xFE821; - case 0xE142: - return 0xFE52F; - case 0xE143: - return 0xFE514; - case 0xE144: - return 0xFEB86; - case 0xE145: - return 0xFEB87; - case 0xE146: - return 0xFE00B; - case 0xE147: - return 0xFE965; - case 0xE148: - return 0xFE546; - case 0xE149: - return 0xFE4DE; - case 0xE14A: - return 0xFE4DF; - case 0xE14B: - return 0xFE531; - case 0xE14C: - return 0xFEB5E; - case 0xE14D: - return 0xFE4B5; - case 0xE14E: - return 0xFE7F7; - case 0xE14F: - return 0xFE7F6; - case 0xE150: - return 0xFE7E7; - case 0xE151: - return 0xFE506; - case 0xE152: - return 0xFE1A1; - case 0xE153: - return 0xFE4B3; - case 0xE154: - return 0xFE4B6; - case 0xE155: - return 0xFE4B4; - case 0xE156: - return 0xFE4B9; - case 0xE157: - return 0xFE4BA; - case 0xE158: - return 0xFE4B7; - case 0xE159: - return 0xFE7E6; - case 0xE15A: - return 0xFE7EF; - case 0xE201: - return 0xFE7F0; - case 0xE202: - return 0xFE7E8; - case 0xE203: - return 0xFEB24; - case 0xE204: - return 0xFEB19; - case 0xE205: - return 0xFEB61; - case 0xE206: - return 0xFEB62; - case 0xE207: - return 0xFEB25; - case 0xE208: - return 0xFEB1F; - case 0xE209: - return 0xFE044; - case 0xE20A: - return 0xFEB20; - case 0xE20B: - return 0xFE838; - case 0xE20C: - return 0xFEB1A; - case 0xE20D: - return 0xFEB1C; - case 0xE20E: - return 0xFEB1B; - case 0xE20F: - return 0xFEB1D; - case 0xE210: - return 0xFE82C; - case 0xE211: - return 0xFE82B; - case 0xE212: - return 0xFEB36; - case 0xE213: - return 0xFEB37; - case 0xE214: - return 0xFEB38; - case 0xE215: - return 0xFEB39; - case 0xE216: - return 0xFEB3A; - case 0xE217: - return 0xFEB3B; - case 0xE218: - return 0xFEB3C; - case 0xE219: - return 0xFEB63; - case 0xE21A: - return 0xFEB64; - case 0xE21B: - return 0xFEB67; - case 0xE21C: - return 0xFE82E; - case 0xE21D: - return 0xFE82F; - case 0xE21E: - return 0xFE830; - case 0xE21F: - return 0xFE831; - case 0xE220: - return 0xFE832; - case 0xE221: - return 0xFE833; - case 0xE222: - return 0xFE834; - case 0xE223: - return 0xFE835; - case 0xE224: - return 0xFE836; - case 0xE225: - return 0xFE837; - case 0xE226: - return 0xFEB3D; - case 0xE227: - return 0xFEB3E; - case 0xE228: - return 0xFEB3F; - case 0xE229: - return 0xFEB81; - case 0xE22A: - return 0xFEB31; - case 0xE22B: - return 0xFEB2F; - case 0xE22C: - return 0xFEB40; - case 0xE22D: - return 0xFEB41; - case 0xE22E: - return 0xFEB99; - case 0xE22F: - return 0xFEB9A; - case 0xE230: - return 0xFEB9B; - case 0xE231: - return 0xFEB9C; - case 0xE232: - return 0xFEAF8; - case 0xE233: - return 0xFEAF9; - case 0xE234: - return 0xFEAFA; - case 0xE235: - return 0xFEAFB; - case 0xE236: - return 0xFEAF0; - case 0xE237: - return 0xFEAF2; - case 0xE238: - return 0xFEAF1; - case 0xE239: - return 0xFEAF3; - case 0xE23A: - return 0xFEAFC; - case 0xE23B: - return 0xFEAFD; - case 0xE23C: - return 0xFEAFE; - case 0xE23D: - return 0xFEAFF; - case 0xE23E: - return 0xFE4F8; - case 0xE23F: - return 0xFE02B; - case 0xE240: - return 0xFE02C; - case 0xE241: - return 0xFE02D; - case 0xE242: - return 0xFE02E; - case 0xE243: - return 0xFE02F; - case 0xE244: - return 0xFE030; - case 0xE245: - return 0xFE031; - case 0xE246: - return 0xFE032; - case 0xE247: - return 0xFE033; - case 0xE248: - return 0xFE034; - case 0xE249: - return 0xFE035; - case 0xE24A: - return 0xFE036; - case 0xE24B: - return 0xFE037; - case 0xE24C: - return 0xFEB42; - case 0xE24D: - return 0xFEB27; - case 0xE24E: - return 0xFEB29; - case 0xE24F: - return 0xFEB2D; - case 0xE250: - return 0xFE839; - case 0xE251: - return 0xFE83A; - case 0xE252: - return 0xFEB23; - case 0xE253: - return 0xFE1B4; - case 0xE254: - return 0xFEE77; - case 0xE255: - return 0xFEE78; - case 0xE256: - return 0xFEE79; - case 0xE257: - return 0xFEE7A; - case 0xE258: - return 0xFEE7B; - case 0xE259: - return 0xFEE7C; - case 0xE25A: - return 0xFEE7D; - case 0xE301: - return 0xFE527; - case 0xE302: - return 0xFE4D3; - case 0xE303: - return 0xFE045; - case 0xE304: - return 0xFE03D; - case 0xE305: - return 0xFE046; - case 0xE306: - return 0xFE828; - case 0xE307: - return 0xFE047; - case 0xE308: - return 0xFE048; - case 0xE309: - return 0xFE508; - case 0xE30A: - return 0xFE803; - case 0xE30B: - return 0xFE985; - case 0xE30C: - return 0xFE987; - case 0xE30D: - return 0xFEB43; - case 0xE30E: - return 0xFEB1E; - case 0xE30F: - return 0xFE50A; - case 0xE310: - return 0xFE516; - case 0xE311: - return 0xFEB58; - case 0xE312: - return 0xFE517; - case 0xE313: - return 0xFE53E; - case 0xE314: - return 0xFE50F; - case 0xE315: - return 0xFEB2B; - case 0xE316: - return 0xFE53C; - case 0xE317: - return 0xFE530; - case 0xE318: - return 0xFE4D4; - case 0xE319: - return 0xFE4D5; - case 0xE31A: - return 0xFE4D7; - case 0xE31B: - return 0xFE4D8; - case 0xE31C: - return 0xFE195; - case 0xE31D: - return 0xFE196; - case 0xE31E: - return 0xFE197; - case 0xE31F: - return 0xFE198; - case 0xE320: - return 0xFE199; - case 0xE321: - return 0xFE4D9; - case 0xE322: - return 0xFE4DA; - case 0xE323: - return 0xFE4F0; - case 0xE324: - return 0xFE808; - case 0xE325: - return 0xFE4F2; - case 0xE326: - return 0xFE814; - case 0xE327: - return 0xFEB0D; - case 0xE328: - return 0xFEB11; - case 0xE329: - return 0xFEB12; - case 0xE32A: - return 0xFEB13; - case 0xE32B: - return 0xFEB14; - case 0xE32C: - return 0xFEB15; - case 0xE32D: - return 0xFEB16; - case 0xE32E: - return 0xFEB60; - case 0xE32F: - return 0xFEB68; - case 0xE330: - return 0xFEB5D; - case 0xE331: - return 0xFEB5B; - case 0xE332: - return 0xFEB44; - case 0xE333: - return 0xFEB45; - case 0xE334: - return 0xFEB57; - case 0xE335: - return 0xFEB69; - case 0xE336: - return 0xFEB0A; - case 0xE337: - return 0xFEB0B; - case 0xE338: - return 0xFE984; - case 0xE339: - return 0xFE964; - case 0xE33A: - return 0xFE966; - case 0xE33B: - return 0xFE967; - case 0xE33C: - return 0xFE968; - case 0xE33D: - return 0xFE969; - case 0xE33E: - return 0xFE96A; - case 0xE33F: - return 0xFE96B; - case 0xE340: - return 0xFE963; - case 0xE341: - return 0xFE96C; - case 0xE342: - return 0xFE961; - case 0xE343: - return 0xFE96D; - case 0xE344: - return 0xFE96E; - case 0xE345: - return 0xFE051; - case 0xE346: - return 0xFE052; - case 0xE347: - return 0xFE053; - case 0xE348: - return 0xFE054; - case 0xE349: - return 0xFE055; - case 0xE34A: - return 0xFE056; - case 0xE34B: - return 0xFE511; - case 0xE34C: - return 0xFE96F; - case 0xE34D: - return 0xFE970; - case 0xE401: - return 0xFE345; - case 0xE402: - return 0xFE343; - case 0xE403: - return 0xFE340; - case 0xE404: - return 0xFE333; - case 0xE405: - return 0xFE347; - case 0xE406: - return 0xFE33C; - case 0xE407: - return 0xFE33F; - case 0xE408: - return 0xFE342; - case 0xE409: - return 0xFE32A; - case 0xE40A: - return 0xFE33E; - case 0xE40B: - return 0xFE33B; - case 0xE40C: - return 0xFE32E; - case 0xE40D: - return 0xFE32F; - case 0xE40E: - return 0xFE326; - case 0xE40F: - return 0xFE325; - case 0xE410: - return 0xFE322; - case 0xE411: - return 0xFE33A; - case 0xE412: - return 0xFE334; - case 0xE413: - return 0xFE339; - case 0xE414: - return 0xFE336; - case 0xE415: - return 0xFE338; - case 0xE416: - return 0xFE33D; - case 0xE417: - return 0xFE32D; - case 0xE418: - return 0xFE32C; - case 0xE419: - return 0xFE190; - case 0xE41A: - return 0xFE192; - case 0xE41B: - return 0xFE191; - case 0xE41C: - return 0xFE193; - case 0xE41D: - return 0xFE35B; - case 0xE41E: - return 0xFEB9D; - case 0xE41F: - return 0xFEB9E; - case 0xE420: - return 0xFEB9F; - case 0xE421: - return 0xFEBA0; - case 0xE422: - return 0xFEBA1; - case 0xE423: - return 0xFE351; - case 0xE424: - return 0xFE352; - case 0xE425: - return 0xFE829; - case 0xE426: - return 0xFE353; - case 0xE427: - return 0xFE358; - case 0xE428: - return 0xFE1A0; - case 0xE429: - return 0xFE1A2; - case 0xE42A: - return 0xFE7D6; - case 0xE42B: - return 0xFE7DD; - case 0xE42C: - return 0xFE80E; - case 0xE42D: - return 0xFE7DE; - case 0xE42E: - return 0xFE7E5; - case 0xE42F: - return 0xFE7F1; - case 0xE430: - return 0xFE7F2; - case 0xE431: - return 0xFE7F3; - case 0xE432: - return 0xFE7F4; - case 0xE433: - return 0xFE7FE; - case 0xE434: - return 0xFE7E0; - case 0xE435: - return 0xFE7E2; - case 0xE436: - return 0xFE518; - case 0xE437: - return 0xFEB17; - case 0xE438: - return 0xFE519; - case 0xE439: - return 0xFE51A; - case 0xE43A: - return 0xFE51B; - case 0xE43B: - return 0xFE51C; - case 0xE43C: - return 0xFE007; - case 0xE43D: - return 0xFE82A; - case 0xE43E: - return 0xFE038; - case 0xE43F: - return 0xFE971; - case 0xE440: - return 0xFE51D; - case 0xE441: - return 0xFE1C6; - case 0xE442: - return 0xFE51E; - case 0xE443: - return 0xFE005; - case 0xE444: - return 0xFE049; - case 0xE445: - return 0xFE51F; - case 0xE446: - return 0xFE017; - case 0xE447: - return 0xFE043; - case 0xE448: - return 0xFE513; - case 0xE449: - return 0xFE00A; - case 0xE44A: - return 0xFE00C; - case 0xE44B: - return 0xFE008; - case 0xE44C: - return 0xFE00D; - case 0xE501: - return 0xFE4B8; - case 0xE502: - return 0xFE804; - case 0xE503: - return 0xFE805; - case 0xE504: - return 0xFE4BD; - case 0xE505: - return 0xFE4BE; - case 0xE506: - return 0xFE4BF; - case 0xE507: - return 0xFE802; - case 0xE508: - return 0xFE4C0; - case 0xE509: - return 0xFE4C4; - case 0xE50A: - return 0xFE4C5; - case 0xE50B: - return 0xFE4E5; - case 0xE50C: - return 0xFE4E6; - case 0xE50D: - return 0xFE4E7; - case 0xE50E: - return 0xFE4E8; - case 0xE50F: - return 0xFE4E9; - case 0xE510: - return 0xFE4EA; - case 0xE511: - return 0xFE4EB; - case 0xE512: - return 0xFE4EC; - case 0xE513: - return 0xFE4ED; - case 0xE514: - return 0xFE4EE; - case 0xE515: - return 0xFE1A4; - case 0xE516: - return 0xFE1A5; - case 0xE517: - return 0xFE1A6; - case 0xE518: - return 0xFE1A7; - case 0xE519: - return 0xFE1A8; - case 0xE51A: - return 0xFE1A9; - case 0xE51B: - return 0xFE1AA; - case 0xE51C: - return 0xFE1AB; - case 0xE51D: - return 0xFE4C6; - case 0xE51E: - return 0xFE1B5; - case 0xE51F: - return 0xFE1B6; - case 0xE520: - return 0xFE1C7; - case 0xE521: - return 0xFE1C8; - case 0xE522: - return 0xFE1C9; - case 0xE523: - return 0xFE1BA; - case 0xE524: - return 0xFE1CA; - case 0xE525: - return 0xFE1CB; - case 0xE526: - return 0xFE1CC; - case 0xE527: - return 0xFE1CD; - case 0xE528: - return 0xFE1CE; - case 0xE529: - return 0xFE1CF; - case 0xE52A: - return 0xFE1D0; - case 0xE52B: - return 0xFE1D1; - case 0xE52C: - return 0xFE1D2; - case 0xE52D: - return 0xFE1D3; - case 0xE52E: - return 0xFE1D4; - case 0xE52F: - return 0xFE1D5; - case 0xE530: - return 0xFE1D6; - case 0xE531: - return 0xFE1D7; - case 0xE532: - return 0xFE50B; - case 0xE533: - return 0xFE50C; - case 0xE534: - return 0xFE50D; - case 0xE535: - return 0xFE50E; - case 0xE536: - return 0xFE553; - case 0xE537: - return 0xFEB2A; - case 0xE538: - return 0xFEE70; - case 0xE539: - return 0xFEE71; - case 0xE53A: - return 0xFEE72; - case 0xE53B: - return 0xFEE73; - case 0xE53C: - return 0xFEE74; - case 0xE53D: - return 0xFEE75; - case 0xE53E: - return 0xFEE76; - default: - return codePoint; - } - } - - public static void setCharset(String charset, Part part) throws MessagingException { - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - part.getMimeType() + ";\r\n charset=" + getExternalCharset(charset)); - } - - public static String getExternalCharset(String charset) { - if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007")) - return "shift_jis"; - - return charset; - } - - public static ViewableContainer extractPartsFromDraft(Message message) - throws MessagingException { - - Body body = message.getBody(); - if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) body; - - ViewableContainer container; - int count = multipart.getCount(); - if (count >= 1) { - // The first part is either a text/plain or a multipart/alternative - BodyPart firstPart = multipart.getBodyPart(0); - container = extractTextual(firstPart); - - // The rest should be attachments - for (int i = 1; i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - container.attachments.add(bodyPart); - } - } else { - container = new ViewableContainer("", "", new ArrayList()); - } - - return container; - } - - return extractTextual(message); - } - - private static ViewableContainer extractTextual(Part part) throws MessagingException { - String text = ""; - String html = ""; - List attachments = new ArrayList(); - - Body firstBody = part.getBody(); - if (part.isMimeType("text/plain")) { - String bodyText = getTextFromPart(part); - if (bodyText != null) { - text = bodyText; - html = HtmlConverter.textToHtml(text); - } - } else if (part.isMimeType("multipart/alternative") && - firstBody instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) firstBody; - for (BodyPart bodyPart : multipart.getBodyParts()) { - String bodyText = getTextFromPart(bodyPart); - if (bodyText != null) { - if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { - text = bodyText; - } else if (html.isEmpty() && bodyPart.isMimeType("text/html")) { - html = bodyText; - } - } - } - } - - return new ViewableContainer(text, html, attachments); - } } diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java index ad6e474ac..d6355662f 100644 --- a/src/com/fsck/k9/mail/internet/TextBody.java +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -4,7 +4,11 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; diff --git a/src/com/fsck/k9/mail/internet/Viewable.java b/src/com/fsck/k9/mail/internet/Viewable.java new file mode 100644 index 000000000..ec21b7c71 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/Viewable.java @@ -0,0 +1,104 @@ +package com.fsck.k9.mail.internet; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + +import java.util.List; + +/** + * Empty marker class interface the class hierarchy used by + * {@link MessageExtractor#getViewables(com.fsck.k9.mail.Part, java.util.List)} + * + * @see Viewable.Text + * @see Viewable.Html + * @see Viewable.MessageHeader + * @see Viewable.Alternative + */ +public interface Viewable { + /** + * Class representing textual parts of a message that aren't marked as attachments. + * + * @see com.fsck.k9.mail.internet.MessageExtractor#isPartTextualBody(com.fsck.k9.mail.Part) + */ + abstract class Textual implements Viewable { + private Part mPart; + + public Textual(Part part) { + mPart = part; + } + + public Part getPart() { + return mPart; + } + } + + /** + * Class representing a {@code text/plain} part of a message. + */ + class Text extends Textual { + public Text(Part part) { + super(part); + } + } + + /** + * Class representing a {@code text/html} part of a message. + */ + class Html extends Textual { + public Html(Part part) { + super(part); + } + } + + /** + * Class representing a {@code message/rfc822} part of a message. + * + *

    + * This is used to extract basic header information when the message contents are displayed + * inline. + *

    + */ + class MessageHeader implements Viewable { + private Part mContainerPart; + private Message mMessage; + + public MessageHeader(Part containerPart, Message message) { + mContainerPart = containerPart; + mMessage = message; + } + + public Part getContainerPart() { + return mContainerPart; + } + + public Message getMessage() { + return mMessage; + } + } + + /** + * Class representing a {@code multipart/alternative} part of a message. + * + *

    + * Only relevant {@code text/plain} and {@code text/html} children are stored in this container + * class. + *

    + */ + class Alternative implements Viewable { + private List mText; + private List mHtml; + + public Alternative(List text, List html) { + mText = text; + mHtml = html; + } + + public List getText() { + return mText; + } + + public List getHtml() { + return mHtml; + } + } +} diff --git a/src/com/fsck/k9/net/ssl/KeyChainKeyManager.java b/src/com/fsck/k9/mail/ssl/KeyChainKeyManager.java similarity index 95% rename from src/com/fsck/k9/net/ssl/KeyChainKeyManager.java rename to src/com/fsck/k9/mail/ssl/KeyChainKeyManager.java index c15fc58ad..273b7dd9a 100644 --- a/src/com/fsck/k9/net/ssl/KeyChainKeyManager.java +++ b/src/com/fsck/k9/mail/ssl/KeyChainKeyManager.java @@ -1,5 +1,5 @@ -package com.fsck.k9.net.ssl; +package com.fsck.k9.mail.ssl; import java.net.Socket; import java.security.Principal; @@ -20,16 +20,17 @@ import android.security.KeyChain; import android.security.KeyChainException; import android.util.Log; -import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.MessagingException; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + /** * For client certificate authentication! Provide private keys and certificates * during the TLS handshake using the Android 4.0 KeyChain API. */ -public class KeyChainKeyManager extends X509ExtendedKeyManager { +class KeyChainKeyManager extends X509ExtendedKeyManager { private static PrivateKey sClientCertificateReferenceWorkaround; @@ -207,10 +208,10 @@ public class KeyChainKeyManager extends X509ExtendedKeyManager { return mAlias; } } - Log.w(K9.LOG_TAG, "Client certificate " + mAlias + " not issued by any of the requested issuers"); + Log.w(LOG_TAG, "Client certificate " + mAlias + " not issued by any of the requested issuers"); return null; } - Log.w(K9.LOG_TAG, "Client certificate " + mAlias + " does not match any of the requested key types"); + Log.w(LOG_TAG, "Client certificate " + mAlias + " does not match any of the requested key types"); return null; } } diff --git a/src/com/fsck/k9/security/LocalKeyStore.java b/src/com/fsck/k9/mail/ssl/LocalKeyStore.java similarity index 95% rename from src/com/fsck/k9/security/LocalKeyStore.java rename to src/com/fsck/k9/mail/ssl/LocalKeyStore.java index d432ab294..cc0587c2c 100644 --- a/src/com/fsck/k9/security/LocalKeyStore.java +++ b/src/com/fsck/k9/mail/ssl/LocalKeyStore.java @@ -1,4 +1,4 @@ -package com.fsck.k9.security; +package com.fsck.k9.mail.ssl; import java.io.File; import java.io.FileInputStream; @@ -15,7 +15,7 @@ import org.apache.commons.io.IOUtils; import android.util.Log; -import com.fsck.k9.K9; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; public class LocalKeyStore { private static final int KEY_STORE_FILE_VERSION = 1; @@ -50,7 +50,7 @@ public class LocalKeyStore { * error, presuming setKeyStoreFile(File) is called next with a * non-null File. */ - Log.w(K9.LOG_TAG, "Local key store has not been initialized"); + Log.w(LOG_TAG, "Local key store has not been initialized"); } } @@ -92,7 +92,7 @@ public class LocalKeyStore { mKeyStore = store; mKeyStoreFile = file; } catch (Exception e) { - Log.e(K9.LOG_TAG, "Failed to initialize local key store", e); + Log.e(LOG_TAG, "Failed to initialize local key store", e); // Use of the local key store is effectively disabled. mKeyStore = null; mKeyStoreFile = null; @@ -169,7 +169,7 @@ public class LocalKeyStore { } catch (KeyStoreException e) { // Ignore: most likely there was no cert. found } catch (CertificateException e) { - Log.e(K9.LOG_TAG, "Error updating the local key store file", e); + Log.e(LOG_TAG, "Error updating the local key store file", e); } } diff --git a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java b/src/com/fsck/k9/mail/ssl/TrustManagerFactory.java similarity index 98% rename from src/com/fsck/k9/net/ssl/TrustManagerFactory.java rename to src/com/fsck/k9/mail/ssl/TrustManagerFactory.java index 4e20f5c67..f6841a2e3 100644 --- a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java +++ b/src/com/fsck/k9/mail/ssl/TrustManagerFactory.java @@ -1,10 +1,9 @@ -package com.fsck.k9.net.ssl; +package com.fsck.k9.mail.ssl; import android.util.Log; import com.fsck.k9.mail.CertificateChainException; -import com.fsck.k9.security.LocalKeyStore; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; diff --git a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java b/src/com/fsck/k9/mail/ssl/TrustedSocketFactory.java similarity index 97% rename from src/com/fsck/k9/net/ssl/TrustedSocketFactory.java rename to src/com/fsck/k9/mail/ssl/TrustedSocketFactory.java index 12188f957..cf74bb899 100644 --- a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java +++ b/src/com/fsck/k9/mail/ssl/TrustedSocketFactory.java @@ -1,4 +1,4 @@ -package com.fsck.k9.net.ssl; +package com.fsck.k9.mail.ssl; import android.util.Log; @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + /** * Filter and reorder list of cipher suites and TLS versions. @@ -90,7 +92,7 @@ public class TrustedSocketFactory { */ supportedProtocols = sock.getSupportedProtocols(); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Error getting information about available SSL/TLS ciphers and " + + Log.e(LOG_TAG, "Error getting information about available SSL/TLS ciphers and " + "protocols", e); } diff --git a/src/com/fsck/k9/mail/store/ImapResponseParser.java b/src/com/fsck/k9/mail/store/ImapResponseParser.java index 4a8c8cc2e..b0e61fb70 100644 --- a/src/com/fsck/k9/mail/store/ImapResponseParser.java +++ b/src/com/fsck/k9/mail/store/ImapResponseParser.java @@ -11,7 +11,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.Locale; -public class ImapResponseParser { +class ImapResponseParser { private static final SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US); private static final SimpleDateFormat badDateTimeFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.US); private static final SimpleDateFormat badDateTimeFormat2 = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z", Locale.US); @@ -293,7 +293,7 @@ public class ImapResponseParser { } catch (Exception e) { // Catch everything else and save it for later. mException = e; - //Log.e(K9.LOG_TAG, "parseLiteral(): Exception in callback method", e); + //Log.e(LOG_TAG, "parseLiteral(): Exception in callback method", e); } // Check if only some of the literal data was read diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index afd4384fd..ae54c870b 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -55,12 +55,8 @@ import android.os.PowerManager; import android.text.TextUtils; import android.util.Log; -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.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.AuthType; @@ -72,13 +68,14 @@ import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.Store; import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream; @@ -90,22 +87,25 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; -import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; -import com.fsck.k9.net.ssl.TrustedSocketFactory; +import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.beetstra.jutf7.CharsetProvider; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; import org.apache.commons.io.IOUtils; +import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; +import static com.fsck.k9.mail.K9MailLib.PUSH_WAKE_LOCK_TIMEOUT; + /** *
      * TODO Need to start keeping track of UIDVALIDITY
      * TODO Need a default response handler for things like folder updates
      * 
    */ -public class ImapStore extends Store { +public class ImapStore extends RemoteStore { public static final String STORE_TYPE = "IMAP"; private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000; @@ -113,7 +113,7 @@ public class ImapStore extends Store { private static int MAX_DELAY_TIME = 5 * 60 * 1000; // 5 minutes private static int NORMAL_DELAY_TIME = 5000; - private static int FETCH_WINDOW_SIZE = 100; + private static final int FETCH_WINDOW_SIZE = 100; private Set mPermanentFlagsIndex = EnumSet.noneOf(Flag.class); @@ -201,19 +201,19 @@ public class ImapStore extends Store { 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]); + username = decodeUtf8(userInfoParts[1]); } else if (userInfoParts.length == 2) { authenticationType = AuthType.PLAIN; - username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); - password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + username = decodeUtf8(userInfoParts[0]); + password = decodeUtf8(userInfoParts[1]); } else if (userInfoParts.length == 3) { authenticationType = AuthType.valueOf(userInfoParts[0]); - username = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + username = decodeUtf8(userInfoParts[1]); if (AuthType.EXTERNAL == authenticationType) { - clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[2]); + clientCertificateAlias = decodeUtf8(userInfoParts[2]); } else { - password = UrlEncodingHelper.decodeUtf8(userInfoParts[2]); + password = decodeUtf8(userInfoParts[2]); } } } @@ -248,15 +248,15 @@ public class ImapStore extends Store { * * @return An ImapStore URI that holds the same information as the {@code server} parameter. * - * @see Account#getStoreUri() + * @see com.fsck.k9.mail.store.StoreConfig#getStoreUri() * @see ImapStore#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String userEnc = encodeUtf8(server.username); String passwordEnc = (server.password != null) ? - UrlEncodingHelper.encodeUtf8(server.password) : ""; + encodeUtf8(server.password) : ""; String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; + encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -387,7 +387,7 @@ public class ImapStore extends Store { @Override public boolean useCompression(final int type) { - return mAccount.useCompression(type); + return mStoreConfig.useCompression(type); } @Override @@ -439,12 +439,12 @@ public class ImapStore extends Store { */ private final Map mFolderCache = new HashMap(); - public ImapStore(Account account) throws MessagingException { - super(account); + public ImapStore(StoreConfig storeConfig) throws MessagingException { + super(storeConfig); ImapStoreSettings settings; try { - settings = decodeUri(mAccount.getStoreUri()); + settings = decodeUri(storeConfig.getStoreUri()); } catch (IllegalArgumentException e) { throw new MessagingException("Error while decoding store URI", e); } @@ -502,7 +502,7 @@ public class ImapStore extends Store { ImapConnection connection = getConnection(); try { List allFolders = listFolders(connection, false); - if (forceListAll || !mAccount.subscribedFoldersOnly()) { + if (forceListAll || !mStoreConfig.subscribedFoldersOnly()) { return allFolders; } else { List resultFolders = new LinkedList(); @@ -544,7 +544,7 @@ public class ImapStore extends Store { boolean includeFolder = true; if (response.size() > 4 || !(response.getObject(3) instanceof String)) { - Log.w(K9.LOG_TAG, "Skipping incorrectly parsed " + commandResponse + + Log.w(LOG_TAG, "Skipping incorrectly parsed " + commandResponse + " reply: " + response); continue; } @@ -553,7 +553,7 @@ public class ImapStore extends Store { try { decodedFolderName = decodeFolderName(response.getString(3)); } catch (CharacterCodingException e) { - Log.w(K9.LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " + + Log.w(LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " + "as defined by RFC 3501: " + response.getString(3), e); //TODO: Use the raw name returned by the server for all commands that require @@ -570,9 +570,9 @@ public class ImapStore extends Store { mCombinedPrefix = null; } - if (folder.equalsIgnoreCase(mAccount.getInboxFolderName())) { + if (folder.equalsIgnoreCase(mStoreConfig.getInboxFolderName())) { continue; - } else if (folder.equals(mAccount.getOutboxFolderName())) { + } else if (folder.equals(mStoreConfig.getOutboxFolderName())) { /* * There is a folder on the server with the same name as our local * outbox. Until we have a good plan to deal with this situation @@ -604,7 +604,7 @@ public class ImapStore extends Store { } } } - folders.add(getFolder(mAccount.getInboxFolderName())); + folders.add(getFolder(mStoreConfig.getInboxFolderName())); return folders; } @@ -624,14 +624,14 @@ public class ImapStore extends Store { String commandOptions = ""; if (connection.capabilities.contains("XLIST")) { - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration: Using XLIST."); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using XLIST."); commandResponse = "XLIST"; } else if(connection.capabilities.contains("SPECIAL-USE")) { - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration: Using RFC6154/SPECIAL-USE."); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using RFC6154/SPECIAL-USE."); commandResponse = "LIST"; commandOptions = " (SPECIAL-USE)"; } else { - if (K9.DEBUG) Log.d(K9.LOG_TAG, "No detected folder auto-configuration methods."); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "No detected folder auto-configuration methods."); return; } @@ -646,7 +646,7 @@ public class ImapStore extends Store { try { decodedFolderName = decodeFolderName(response.getString(3)); } catch (CharacterCodingException e) { - Log.w(K9.LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " + + Log.w(LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " + "as defined by RFC 3501: " + response.getString(3), e); // We currently just skip folders with malformed names. continue; @@ -661,18 +661,18 @@ public class ImapStore extends Store { for (int i = 0, count = attributes.size(); i < count; i++) { String attribute = attributes.getString(i); if (attribute.equals("\\Drafts")) { - mAccount.setDraftsFolderName(decodedFolderName); - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration detected draft folder: " + decodedFolderName); + mStoreConfig.setDraftsFolderName(decodedFolderName); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected draft folder: " + decodedFolderName); } else if (attribute.equals("\\Sent")) { - mAccount.setSentFolderName(decodedFolderName); - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration detected sent folder: " + decodedFolderName); + mStoreConfig.setSentFolderName(decodedFolderName); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected sent folder: " + decodedFolderName); } else if (attribute.equals("\\Spam") || attribute.equals("\\Junk")) { //rfc6154 just mentions \Junk - mAccount.setSpamFolderName(decodedFolderName); - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration detected spam folder: " + decodedFolderName); + mStoreConfig.setSpamFolderName(decodedFolderName); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected spam folder: " + decodedFolderName); } else if (attribute.equals("\\Trash")) { - mAccount.setTrashFolderName(decodedFolderName); - if (K9.DEBUG) Log.d(K9.LOG_TAG, "Folder auto-configuration detected trash folder: " + decodedFolderName); + mStoreConfig.setTrashFolderName(decodedFolderName); + if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected trash folder: " + decodedFolderName); } } } @@ -777,7 +777,7 @@ public class ImapStore extends Store { } - class ImapFolder extends Folder { + class ImapFolder extends Folder { private String mName; protected volatile int mMessageCount = -1; protected volatile long uidNext = -1L; @@ -789,14 +789,14 @@ public class ImapStore extends Store { private boolean mInSearch = false; public ImapFolder(ImapStore nStore, String name) { - super(nStore.getAccount()); + super(); store = nStore; this.mName = name; } public String getPrefixedName() throws MessagingException { String prefixedName = ""; - if (!mAccount.getInboxFolderName().equalsIgnoreCase(mName)) { + if (!mStoreConfig.getInboxFolderName().equalsIgnoreCase(mName)) { ImapConnection connection = null; synchronized (this) { if (mConnection == null) { @@ -914,7 +914,7 @@ public class ImapStore extends Store { } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Unable to open connection for " + getLogId(), me); + Log.e(LOG_TAG, "Unable to open connection for " + getLogId(), me); throw me; } } @@ -968,7 +968,7 @@ public class ImapStore extends Store { synchronized (this) { // If we are mid-search and we get a close request, we gotta trash the connection. if (mInSearch && mConnection != null) { - Log.i(K9.LOG_TAG, "IMAP search was aborted, shutting down connection."); + Log.i(LOG_TAG, "IMAP search was aborted, shutting down connection."); mConnection.close(); } else { releaseConnection(mConnection); @@ -1117,8 +1117,8 @@ public class ImapStore extends Store { /* * If the remote folder doesn't exist we try to create it. */ - if (K9.DEBUG) { - Log.i(K9.LOG_TAG, "ImapFolder.copyMessages: attempting to create remote " + + if (K9MailLib.isDebug()) { + Log.i(LOG_TAG, "ImapFolder.copyMessages: attempting to create remote " + "folder '" + remoteDestName + "' for " + getLogId()); } @@ -1127,7 +1127,7 @@ public class ImapStore extends Store { //TODO: Split this into multiple commands if the command exceeds a certain length. List responses = executeSimpleCommand(String.format("UID COPY %s %s", - Utility.combine(uids, ','), + combine(uids, ','), remoteDestName)); // Get the tagged response for the UID COPY command @@ -1169,15 +1169,15 @@ public class ImapStore extends Store { uidMap.put(srcUid, destUid); } } else { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Parse error: size of source UIDs " + + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Parse error: size of source UIDs " + "list is not the same as size of destination " + "UIDs list."); } } } else { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Parsing of the sequence set failed."); } } } @@ -1214,14 +1214,14 @@ public class ImapStore extends Store { /* * If the remote trash folder doesn't exist we try to create it. */ - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "IMAPMessage.delete: attempting to create remote '" + trashFolderName + "' folder for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "IMAPMessage.delete: attempting to create remote '" + trashFolderName + "' folder for " + getLogId()); remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); } if (exists(remoteTrashName)) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "IMAPMessage.delete: copying remote " + messages.size() + " messages to '" + trashFolderName + "' for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "IMAPMessage.delete: copying remote " + messages.size() + " messages to '" + trashFolderName + "' for " + getLogId()); moveMessages(messages, remoteTrashFolder); } else { @@ -1281,7 +1281,7 @@ public class ImapStore extends Store { return Long.parseLong(messages.get(0).getUid()); } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to find highest UID in folder " + getName(), e); + Log.e(LOG_TAG, "Unable to find highest UID in folder " + getName(), e); } return -1L; @@ -1293,7 +1293,7 @@ public class ImapStore extends Store { } @Override - public Message getMessage(String uid) throws MessagingException { + public ImapMessage getMessage(String uid) throws MessagingException { return new ImapMessage(uid, this); } @@ -1334,7 +1334,7 @@ public class ImapStore extends Store { 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 executeSimpleCommand(String.format("UID SEARCH %s%s", combine(mesgSeqs.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); } }; return search(searcher, listener); @@ -1345,13 +1345,13 @@ public class ImapStore extends Store { 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 executeSimpleCommand(String.format("UID SEARCH UID %s%s", combine(mesgUids.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); } }; return search(searcher, listener); } - private List search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException { + private List search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException { checkOpen(); //only need READ access List messages = new ArrayList(); @@ -1432,7 +1432,7 @@ public class ImapStore extends Store { } @Override - public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { if (messages == null || messages.isEmpty()) { return; @@ -1461,15 +1461,15 @@ public class ImapStore extends Store { fetchFields.add("INTERNALDATE"); fetchFields.add("RFC822.SIZE"); fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " + - "reply-to message-id references in-reply-to " + K9.IDENTITY_HEADER + ")]"); + "reply-to message-id references in-reply-to " + K9MailLib.IDENTITY_HEADER + ")]"); } if (fp.contains(FetchProfile.Item.STRUCTURE)) { fetchFields.add("BODYSTRUCTURE"); } if (fp.contains(FetchProfile.Item.BODY_SANE)) { // If the user wants to download unlimited-size messages, don't go only for the truncated body - if (mAccount.getMaximumAutoDownloadMessageSize() > 0) { - fetchFields.add(String.format(Locale.US, "BODY.PEEK[]<0.%d>", mAccount.getMaximumAutoDownloadMessageSize())); + if (mStoreConfig.getMaximumAutoDownloadMessageSize() > 0) { + fetchFields.add(String.format(Locale.US, "BODY.PEEK[]<0.%d>", mStoreConfig.getMaximumAutoDownloadMessageSize())); } else { fetchFields.add("BODY.PEEK[]"); } @@ -1485,8 +1485,8 @@ public class ImapStore extends Store { try { mConnection.sendCommand(String.format("UID FETCH %s (%s)", - Utility.combine(uidWindow.toArray(new String[uidWindow.size()]), ','), - Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') + combine(uidWindow.toArray(new String[uidWindow.size()]), ','), + combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') ), false); ImapResponse response; int messageNumber = 0; @@ -1506,18 +1506,18 @@ public class ImapStore extends Store { if (uid != null) { try { msgSeqUidMap.put(msgSeq, uid); - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Stored uid '" + uid + "' for msgSeq " + msgSeq + " into map " /*+ msgSeqUidMap.toString() */); + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Stored uid '" + uid + "' for msgSeq " + msgSeq + " into map " /*+ msgSeqUidMap.toString() */); } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to store uid '" + uid + "' for msgSeq " + msgSeq); + Log.e(LOG_TAG, "Unable to store uid '" + uid + "' for msgSeq " + msgSeq); } } Message message = messageMap.get(uid); if (message == null) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Do not have message in messageMap for UID " + uid + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Do not have message in messageMap for UID " + uid + " for " + getLogId()); handleUntaggedResponse(response); continue; @@ -1544,7 +1544,7 @@ public class ImapStore extends Store { } if (listener != null) { - listener.messageFinished(message, messageNumber, messageMap.size()); + listener.messageFinished(imapMessage, messageNumber, messageMap.size()); } } else { handleUntaggedResponse(response); @@ -1572,7 +1572,7 @@ public class ImapStore extends Store { String partId = parts[0]; if ("TEXT".equalsIgnoreCase(partId)) { fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>", - mAccount.getMaximumAutoDownloadMessageSize()); + mStoreConfig.getMaximumAutoDownloadMessageSize()); } else { fetch = String.format("BODY.PEEK[%s]", partId); } @@ -1596,8 +1596,8 @@ public class ImapStore extends Store { String uid = fetchList.getKeyedString("UID"); if (!message.getUid().equals(uid)) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Did not ask for UID " + uid + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Did not ask for UID " + uid + " for " + getLogId()); handleUntaggedResponse(response); continue; @@ -1684,8 +1684,8 @@ public class ImapStore extends Store { try { parseBodyStructure(bs, message, "TEXT"); } catch (MessagingException e) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Error handling message for " + getLogId(), e); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Error handling message for " + getLogId(), e); message.setBody(null); } } @@ -1733,8 +1733,8 @@ public class ImapStore extends Store { String key = (String)keyObj; if ("UIDNEXT".equalsIgnoreCase(key)) { uidNext = bracketed.getLong(1); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got UidNext = " + uidNext + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got UidNext = " + uidNext + " for " + getLogId()); } } } @@ -1752,15 +1752,15 @@ public class ImapStore extends Store { if (response.mTag == null && response.size() > 1) { if (ImapResponseParser.equalsIgnoreCase(response.get(1), "EXISTS")) { mMessageCount = response.getNumber(0); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got untagged EXISTS with value " + mMessageCount + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got untagged EXISTS with value " + mMessageCount + " for " + getLogId()); } handlePossibleUidNext(response); if (ImapResponseParser.equalsIgnoreCase(response.get(1), "EXPUNGE") && mMessageCount > 0) { mMessageCount--; - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got untagged EXPUNGE with mMessageCount " + mMessageCount + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got untagged EXPUNGE with mMessageCount " + mMessageCount + " for " + getLogId()); } // if (response.size() > 1) { // Object bracketedObj = response.get(1); @@ -1782,7 +1782,7 @@ public class ImapStore extends Store { // sb.append(' '); // } // -// Log.w(K9.LOG_TAG, "ALERT: " + sb.toString() + " for " + getLogId()); +// Log.w(LOG_TAG, "ALERT: " + sb.toString() + " for " + getLogId()); // } // } // } @@ -1791,7 +1791,7 @@ public class ImapStore extends Store { // } // } } - //Log.i(K9.LOG_TAG, "mMessageCount = " + mMessageCount + " for " + getLogId()); + //Log.i(LOG_TAG, "mMessageCount = " + mMessageCount + " for " + getLogId()); } private void parseBodyStructure(ImapList bs, Part part, String id) @@ -2020,8 +2020,8 @@ public class ImapStore extends Store { * not implement the APPENDUID response code. */ String newUid = getUidFromMessageId(message); - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); } if (!TextUtils.isEmpty(newUid)) { @@ -2051,13 +2051,13 @@ public class ImapStore extends Store { String[] messageIdHeader = message.getHeader("Message-ID"); if (messageIdHeader == null || messageIdHeader.length == 0) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Did not get a message-id in order to search for UID for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Did not get a message-id in order to search for UID for " + getLogId()); return null; } String messageId = messageIdHeader[0]; - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Looking for UID for message with message-id " + messageId + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Looking for UID for message with message-id " + messageId + " for " + getLogId()); List responses = executeSimpleCommand( @@ -2103,7 +2103,7 @@ public class ImapStore extends Store { } } - return Utility.combine(flagNames.toArray(new String[flagNames.size()]), ' '); + return combine(flagNames.toArray(new String[flagNames.size()]), ' '); } @@ -2136,7 +2136,7 @@ public class ImapStore extends Store { return null; } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Exception while updated push state for " + getLogId(), e); + Log.e(LOG_TAG, "Exception while updated push state for " + getLogId(), e); return null; } } @@ -2153,7 +2153,7 @@ public class ImapStore extends Store { } try { executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", - Utility.combine(uids, ','), + combine(uids, ','), value ? "+" : "-", combineFlags(flags))); } catch (IOException ioe) { @@ -2168,7 +2168,7 @@ public class ImapStore extends Store { } private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { - Log.e(K9.LOG_TAG, "IOException for " + getLogId(), ioe); + Log.e(LOG_TAG, "IOException for " + getLogId(), ioe); if (connection != null) { connection.close(); } @@ -2194,7 +2194,7 @@ public class ImapStore extends Store { } protected String getLogId() { - String id = getAccount().getDescription() + ":" + getName() + "/" + Thread.currentThread().getName(); + String id = mStoreConfig.toString() + ":" + getName() + "/" + Thread.currentThread().getName(); if (mConnection != null) { id += "/" + mConnection.getLogId(); } @@ -2213,7 +2213,7 @@ public class ImapStore extends Store { public List search(final String queryString, final Set requiredFlags, final Set forbiddenFlags) throws MessagingException { - if (!mAccount.allowRemoteSearch()) { + if (!mStoreConfig.allowRemoteSearch()) { throw new MessagingException("Your settings do not allow remote searching of this account"); } @@ -2287,7 +2287,7 @@ public class ImapStore extends Store { } } final String encodedQry = encodeString(queryString); - if (mAccount.isRemoteSearchFullText()) { + if (mStoreConfig.isRemoteSearchFullText()) { imapQuery += "TEXT " + encodedQry; } else { imapQuery += "OR SUBJECT " + encodedQry + " FROM " + encodedQry; @@ -2351,14 +2351,14 @@ public class ImapStore extends Store { if (capabilityList != null && !capabilityList.isEmpty() && ImapResponseParser.equalsIgnoreCase(capabilityList.get(0), CAPABILITY_CAPABILITY)) { - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Saving " + capabilityList.size() + " capabilities for " + getLogId()); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "Saving " + capabilityList.size() + " capabilities for " + getLogId()); } for (Object capability : capabilityList) { if (capability instanceof String) { -// if (K9.DEBUG) +// if (K9MailLib.isDebug()) // { -// Log.v(K9.LOG_TAG, "Saving capability '" + capability + "' for " + getLogId()); +// Log.v(LOG_TAG, "Saving capability '" + capability + "' for " + getLogId()); // } capabilities.add(((String)capability).toUpperCase(Locale.US)); } @@ -2379,14 +2379,14 @@ public class ImapStore extends Store { try { Security.setProperty("networkaddress.cache.ttl", "0"); } catch (Exception e) { - Log.w(K9.LOG_TAG, "Could not set DNS ttl to 0 for " + getLogId(), e); + Log.w(LOG_TAG, "Could not set DNS ttl to 0 for " + getLogId(), e); } try { Security.setProperty("networkaddress.cache.negative.ttl", "0"); } catch (Exception e) { - Log.w(K9.LOG_TAG, "Could not set DNS negative ttl to 0 for " + getLogId(), e); + Log.w(LOG_TAG, "Could not set DNS negative ttl to 0 for " + getLogId(), e); } try { @@ -2396,8 +2396,8 @@ public class ImapStore extends Store { InetAddress[] addresses = InetAddress.getAllByName(mSettings.getHost()); for (int i = 0; i < addresses.length; i++) { try { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) { - Log.d(K9.LOG_TAG, "Connecting to " + mSettings.getHost() + " as " + + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + Log.d(LOG_TAG, "Connecting to " + mSettings.getHost() + " as " + addresses[i]); } @@ -2424,7 +2424,7 @@ public class ImapStore extends Store { } } - setReadTimeout(Store.SOCKET_READ_TIMEOUT); + setReadTimeout(SOCKET_READ_TIMEOUT); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); @@ -2433,16 +2433,16 @@ public class ImapStore extends Store { capabilities.clear(); ImapResponse nullResponse = mParser.readResponse(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) - Log.v(K9.LOG_TAG, getLogId() + "<<<" + nullResponse); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + nullResponse); List nullResponses = new LinkedList(); nullResponses.add(nullResponse); receiveCapabilities(nullResponses); if (!hasCapability(CAPABILITY_CAPABILITY)) { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId()); List responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY)); if (responses.size() != 2) { throw new MessagingException("Invalid CAPABILITY response received"); @@ -2458,14 +2458,14 @@ public class ImapStore extends Store { mSocket = TrustedSocketFactory.createSocket(mSocket, mSettings.getHost(), mSettings.getPort(), mSettings.getClientCertificateAlias()); - mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); mIn = new PeekableInputStream(new BufferedInputStream(mSocket .getInputStream(), 1024)); mParser = new ImapResponseParser(mIn); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); // Per RFC 2595 (3.1): Once TLS has been started, reissue CAPABILITY command - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId()); capabilities.clear(); List responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY)); if (responses.size() != 2) { @@ -2519,8 +2519,8 @@ public class ImapStore extends Store { "Unhandled authentication method found in the server settings (bug)."); } authSuccess = true; - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(CAPABILITY_COMPRESS_DEFLATE)); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(CAPABILITY_COMPRESS_DEFLATE)); } if (hasCapability(CAPABILITY_COMPRESS_DEFLATE)) { ConnectivityManager connectivityManager = (ConnectivityManager)K9.app.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -2529,13 +2529,13 @@ public class ImapStore extends Store { NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); if (netInfo != null) { int type = netInfo.getType(); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "On network type " + type); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "On network type " + type); useCompression = mSettings.useCompression(type); } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "useCompression " + useCompression); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "useCompression " + useCompression); if (useCompression) { try { executeSimpleCommand(COMMAND_COMPRESS_DEFLATE); @@ -2546,53 +2546,53 @@ public class ImapStore extends Store { ZOutputStream zOutputStream = new ZOutputStream(mSocket.getOutputStream(), JZlib.Z_BEST_SPEED, true); mOut = new BufferedOutputStream(zOutputStream, 1024); zOutputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH); - if (K9.DEBUG) { - Log.i(K9.LOG_TAG, "Compression enabled for " + getLogId()); + if (K9MailLib.isDebug()) { + Log.i(LOG_TAG, "Compression enabled for " + getLogId()); } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to negotiate compression", e); + Log.e(LOG_TAG, "Unable to negotiate compression", e); } } } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE) + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE) + ", mPathPrefix = " + mSettings.getPathPrefix()); if (mSettings.getPathPrefix() == null) { if (hasCapability(CAPABILITY_NAMESPACE)) { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "mPathPrefix is unset and server has NAMESPACE capability"); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "mPathPrefix is unset and server has NAMESPACE capability"); List namespaceResponses = executeSimpleCommand(COMMAND_NAMESPACE); for (ImapResponse response : namespaceResponses) { if (ImapResponseParser.equalsIgnoreCase(response.get(0), COMMAND_NAMESPACE)) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got NAMESPACE response " + response + " on " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got NAMESPACE response " + response + " on " + getLogId()); Object personalNamespaces = response.get(1); if (personalNamespaces != null && personalNamespaces instanceof ImapList) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got personal namespaces: " + personalNamespaces); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got personal namespaces: " + personalNamespaces); ImapList bracketed = (ImapList)personalNamespaces; Object firstNamespace = bracketed.get(0); if (firstNamespace != null && firstNamespace instanceof ImapList) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got first personal namespaces: " + firstNamespace); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got first personal namespaces: " + firstNamespace); bracketed = (ImapList)firstNamespace; mSettings.setPathPrefix(bracketed.getString(0)); mSettings.setPathDelimeter(bracketed.getString(1)); mSettings.setCombinedPrefix(null); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got path '" + mSettings.getPathPrefix() + "' and separator '" + mSettings.getPathDelimeter() + "'"); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got path '" + mSettings.getPathPrefix() + "' and separator '" + mSettings.getPathDelimeter() + "'"); } } } } } else { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability"); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability"); mSettings.setPathPrefix(""); } } @@ -2604,12 +2604,12 @@ public class ImapStore extends Store { if (ImapResponseParser.equalsIgnoreCase(response.get(0), "LIST")) { mSettings.setPathDelimeter(response.getString(2)); mSettings.setCombinedPrefix(null); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got path delimeter '" + mSettings.getPathDelimeter() + "' for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got path delimeter '" + mSettings.getPathDelimeter() + "' for " + getLogId()); } } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to get path delimeter using LIST", e); + Log.e(LOG_TAG, "Unable to get path delimeter using LIST", e); } } @@ -2626,14 +2626,14 @@ public class ImapStore extends Store { String ceMess = ce.getMessage(); String[] tokens = ceMess.split("-"); if (tokens != null && tokens.length > 1 && tokens[1] != null) { - Log.e(K9.LOG_TAG, "Stripping host/port from ConnectionException for " + getLogId(), ce); + Log.e(LOG_TAG, "Stripping host/port from ConnectionException for " + getLogId(), ce); throw new ConnectException(tokens[1].trim()); } else { throw ce; } } finally { if (!authSuccess) { - Log.e(K9.LOG_TAG, "Failed to login, closing connection for " + getLogId()); + Log.e(LOG_TAG, "Failed to login, closing connection for " + getLogId()); close(); } } @@ -2703,7 +2703,7 @@ public class ImapStore extends Store { try { receiveCapabilities(executeSimpleCommand( String.format("AUTHENTICATE EXTERNAL %s", - Utility.base64Encode(mSettings.getUsername())), false)); + Base64.encode(mSettings.getUsername())), false)); } catch (ImapException e) { /* * Provide notification to the user of a problem authenticating @@ -2726,7 +2726,7 @@ public class ImapStore extends Store { throw new MessagingException( "Command continuation aborted: " + response); } else { - Log.w(K9.LOG_TAG, "After sending tag " + tag + Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId()); } @@ -2742,11 +2742,11 @@ public class ImapStore extends Store { ImapResponse response; do { response = mParser.readResponse(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) - Log.v(K9.LOG_TAG, getLogId() + "<<<" + response); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + response); if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) { - Log.w(K9.LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId()); + Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId()); Iterator iter = responses.iterator(); while (iter.hasNext()) { ImapResponse delResponse = iter.next(); @@ -2777,8 +2777,8 @@ public class ImapStore extends Store { } protected boolean isIdleCapable() { - if (K9.DEBUG) - Log.v(K9.LOG_TAG, "Connection " + getLogId() + " has " + capabilities.size() + " capabilities"); + if (K9MailLib.isDebug()) + Log.v(LOG_TAG, "Connection " + getLogId() + " has " + capabilities.size() + " capabilities"); return capabilities.contains(CAPABILITY_IDLE); } @@ -2814,8 +2814,8 @@ public class ImapStore extends Store { public ImapResponse readResponse(ImapResponseParser.IImapResponseCallback callback) throws IOException { try { ImapResponse response = mParser.readResponse(callback); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) - Log.v(K9.LOG_TAG, getLogId() + "<<<" + response); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + response); return response; } catch (IOException ioe) { @@ -2830,8 +2830,8 @@ public class ImapStore extends Store { mOut.write('\n'); mOut.flush(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) - Log.v(K9.LOG_TAG, getLogId() + ">>> " + continuation); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + ">>> " + continuation); } @@ -2844,12 +2844,12 @@ public class ImapStore extends Store { mOut.write(commandToSend.getBytes()); mOut.flush(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP) { - if (sensitive && !K9.DEBUG_SENSITIVE) { - Log.v(K9.LOG_TAG, getLogId() + ">>> " + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + if (sensitive && !K9MailLib.isDebugSensitive()) { + Log.v(LOG_TAG, getLogId() + ">>> " + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); } else { - Log.v(K9.LOG_TAG, getLogId() + ">>> " + commandToSend); + Log.v(LOG_TAG, getLogId() + ">>> " + commandToSend); } } @@ -2879,17 +2879,17 @@ public class ImapStore extends Store { public List executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler) throws IOException, ImapException, MessagingException { String commandToLog = command; - if (sensitive && !K9.DEBUG_SENSITIVE) { + if (sensitive && !K9MailLib.isDebugSensitive()) { commandToLog = "*sensitive*"; } - //if (K9.DEBUG) - // Log.v(K9.LOG_TAG, "Sending IMAP command " + commandToLog + " on connection " + getLogId()); + //if (K9MailLib.isDebug()) + // Log.v(LOG_TAG, "Sending IMAP command " + commandToLog + " on connection " + getLogId()); String tag = sendCommand(command, sensitive); - //if (K9.DEBUG) - // Log.v(K9.LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId()); + //if (K9MailLib.isDebug()) + // Log.v(LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId()); return readStatusResponse(tag, commandToLog, untaggedHandler); } @@ -2953,13 +2953,13 @@ public class ImapStore extends Store { super(store, name); receiver = nReceiver; TracingPowerManager pm = TracingPowerManager.getPowerManager(receiver.getContext()); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ImapFolderPusher " + store.getAccount().getDescription() + ":" + getName()); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ImapFolderPusher " + mStoreConfig.toString() + ":" + getName()); wakeLock.setReferenceCounted(false); } public void refresh() throws IOException, MessagingException { if (idling.get()) { - wakeLock.acquire(K9.PUSH_WAKE_LOCK_TIMEOUT); + wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT); sendDone(); } } @@ -2968,7 +2968,7 @@ public class ImapStore extends Store { if (doneSent.compareAndSet(false, true)) { ImapConnection conn = mConnection; if (conn != null) { - conn.setReadTimeout(Store.SOCKET_READ_TIMEOUT); + conn.setReadTimeout(SOCKET_READ_TIMEOUT); sendContinuation("DONE"); } @@ -2987,9 +2987,9 @@ public class ImapStore extends Store { Runnable runner = new Runnable() { @Override public void run() { - wakeLock.acquire(K9.PUSH_WAKE_LOCK_TIMEOUT); - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Pusher starting for " + getLogId()); + wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Pusher starting for " + getLogId()); long lastUidNext = -1L; while (!stop.get()) { @@ -2999,10 +2999,10 @@ public class ImapStore extends Store { String pushStateS = receiver.getPushState(getName()); ImapPushState pushState = ImapPushState.parse(pushStateS); oldUidNext = pushState.uidNext; - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId()); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); + Log.e(LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); } /* @@ -3030,7 +3030,7 @@ public class ImapStore extends Store { throw new MessagingException("IMAP server is not IDLE capable:" + conn.toString()); } - if (!stop.get() && mAccount.isPushPollOnConnect() && (conn != oldConnection || needsPoll.getAndSet(false))) { + if (!stop.get() && mStoreConfig.isPushPollOnConnect() && (conn != oldConnection || needsPoll.getAndSet(false))) { List untaggedResponses = new ArrayList(storedUntaggedResponses); storedUntaggedResponses.clear(); processUntaggedResponses(untaggedResponses); @@ -3047,22 +3047,22 @@ public class ImapStore extends Store { long newUidNext = uidNext; if (newUidNext == -1) { - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "uidNext is -1, using search to find highest UID"); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "uidNext is -1, using search to find highest UID"); } long highestUid = getHighestUid(); if (highestUid != -1L) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "highest UID = " + highestUid); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "highest UID = " + highestUid); newUidNext = highestUid + 1; - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "highest UID = " + highestUid + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "highest UID = " + highestUid + ", set newUidNext to " + newUidNext); } } - if (startUid < newUidNext - mAccount.getDisplayCount()) { - startUid = newUidNext - mAccount.getDisplayCount(); + if (startUid < newUidNext - mStoreConfig.getDisplayCount()) { + startUid = newUidNext - mStoreConfig.getDisplayCount(); } if (startUid < 1) { startUid = 1; @@ -3071,8 +3071,8 @@ public class ImapStore extends Store { lastUidNext = newUidNext; if (newUidNext > startUid) { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Needs sync from uid " + startUid + " to " + newUidNext + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Needs sync from uid " + startUid + " to " + newUidNext + " for " + getLogId()); List messages = new ArrayList(); for (long uid = startUid; uid < newUidNext; uid++) { ImapMessage message = new ImapMessage("" + uid, ImapFolderPusher.this); @@ -3085,41 +3085,41 @@ public class ImapStore extends Store { } else { List untaggedResponses = null; while (!storedUntaggedResponses.isEmpty()) { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Processing " + storedUntaggedResponses.size() + " untagged responses from previous commands for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Processing " + storedUntaggedResponses.size() + " untagged responses from previous commands for " + getLogId()); untaggedResponses = new ArrayList(storedUntaggedResponses); storedUntaggedResponses.clear(); processUntaggedResponses(untaggedResponses); } - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "About to IDLE for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "About to IDLE for " + getLogId()); receiver.setPushActive(getName(), true); idling.set(true); doneSent.set(false); - conn.setReadTimeout((getAccount().getIdleRefreshMinutes() * 60 * 1000) + IDLE_READ_TIMEOUT_INCREMENT); + conn.setReadTimeout((mStoreConfig.getIdleRefreshMinutes() * 60 * 1000) + IDLE_READ_TIMEOUT_INCREMENT); untaggedResponses = executeSimpleCommand(COMMAND_IDLE, false, ImapFolderPusher.this); idling.set(false); delayTime.set(NORMAL_DELAY_TIME); idleFailureCount.set(0); } } catch (Exception e) { - wakeLock.acquire(K9.PUSH_WAKE_LOCK_TIMEOUT); + wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT); storedUntaggedResponses.clear(); idling.set(false); receiver.setPushActive(getName(), false); try { close(); } catch (Exception me) { - Log.e(K9.LOG_TAG, "Got exception while closing for exception for " + getLogId(), me); + Log.e(LOG_TAG, "Got exception while closing for exception for " + getLogId(), me); } if (stop.get()) { - Log.i(K9.LOG_TAG, "Got exception while idling, but stop is set for " + getLogId()); + Log.i(LOG_TAG, "Got exception while idling, but stop is set for " + getLogId()); } else { receiver.pushError("Push error for " + getName(), e); - Log.e(K9.LOG_TAG, "Got exception while idling for " + getLogId(), e); + Log.e(LOG_TAG, "Got exception while idling for " + getLogId(), e); int delayTimeInt = delayTime.get(); receiver.sleep(wakeLock, delayTimeInt); delayTimeInt *= 2; @@ -3128,7 +3128,7 @@ public class ImapStore extends Store { } delayTime.set(delayTimeInt); if (idleFailureCount.incrementAndGet() > IDLE_FAILURE_COUNT_LIMIT) { - Log.e(K9.LOG_TAG, "Disabling pusher for " + getLogId() + " after " + idleFailureCount.get() + " consecutive errors"); + Log.e(LOG_TAG, "Disabling pusher for " + getLogId() + " after " + idleFailureCount.get() + " consecutive errors"); receiver.pushError("Push disabled for " + getName() + " after " + idleFailureCount.get() + " consecutive errors", e); stop.set(true); } @@ -3138,11 +3138,11 @@ public class ImapStore extends Store { } receiver.setPushActive(getName(), false); try { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Pusher for " + getLogId() + " is exiting"); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Pusher for " + getLogId() + " is exiting"); close(); } catch (Exception me) { - Log.e(K9.LOG_TAG, "Got exception while closing for " + getLogId(), me); + Log.e(LOG_TAG, "Got exception while closing for " + getLogId(), me); } finally { wakeLock.release(); } @@ -3159,8 +3159,8 @@ public class ImapStore extends Store { if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH") || ImapResponseParser.equalsIgnoreCase(responseType, "EXPUNGE") || ImapResponseParser.equalsIgnoreCase(responseType, "EXISTS")) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Storing response " + response + " for later processing"); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Storing response " + response + " for later processing"); storedUntaggedResponses.add(response); } @@ -3188,8 +3188,8 @@ public class ImapStore extends Store { syncMessages(mMessageCount, true); } } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "UIDs for messages needing flag sync are " + flagSyncMsgSeqs + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "UIDs for messages needing flag sync are " + flagSyncMsgSeqs + " for " + getLogId()); if (!flagSyncMsgSeqs.isEmpty()) { syncMessages(flagSyncMsgSeqs); @@ -3205,17 +3205,17 @@ public class ImapStore extends Store { String pushStateS = receiver.getPushState(getName()); ImapPushState pushState = ImapPushState.parse(pushStateS); oldUidNext = pushState.uidNext; - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId()); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); + Log.e(LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); } 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()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Got newUid " + newUid + " for message " + end + " on " + getLogId()); long startUid = oldUidNext; if (startUid < newUid - 10) { startUid = newUid - 10; @@ -3225,8 +3225,8 @@ public class ImapStore extends Store { } if (newUid >= startUid) { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Needs sync from uid " + startUid + " to " + newUid + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Needs sync from uid " + startUid + " to " + newUid + " for " + getLogId()); List messages = new ArrayList(); for (long uid = startUid; uid <= newUid; uid++) { ImapMessage message = new ImapMessage(Long.toString(uid), ImapFolderPusher.this); @@ -3261,7 +3261,7 @@ public class ImapStore extends Store { needsPoll.set(true); msgSeqUidMap.clear(); String existingUid = existingMessage.getUid(); - Log.w(K9.LOG_TAG, "Message with UID " + existingUid + " still exists on server, not expunging"); + Log.w(LOG_TAG, "Message with UID " + existingUid + " still exists on server, not expunging"); removeUids.remove(existingUid); } for (String uid : removeUids) { @@ -3269,13 +3269,13 @@ public class ImapStore extends Store { try { message.setFlagInternal(Flag.DELETED, true); } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Unable to set DELETED flag on message " + message.getUid()); + Log.e(LOG_TAG, "Unable to set DELETED flag on message " + message.getUid()); } messages.add(message); } receiver.messagesRemoved(this, messages); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Cannot remove EXPUNGEd messages", e); + Log.e(LOG_TAG, "Cannot remove EXPUNGEd messages", e); } } @@ -3287,11 +3287,11 @@ public class ImapStore extends Store { try { Object responseType = response.get(1); if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")) { - Log.i(K9.LOG_TAG, "Got FETCH " + response); + Log.i(LOG_TAG, "Got FETCH " + response); long msgSeq = response.getLong(0); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq + " for " + getLogId()); if (!flagSyncMsgSeqs.contains(msgSeq)) { flagSyncMsgSeqs.add(msgSeq); @@ -3302,8 +3302,8 @@ public class ImapStore extends Store { if (msgSeq <= oldMessageCount) { messageCountDelta = -1; } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got untagged EXPUNGE for msgseq " + msgSeq + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got untagged EXPUNGE for msgseq " + msgSeq + " for " + getLogId()); List newSeqs = new ArrayList(); Iterator flagIter = flagSyncMsgSeqs.iterator(); @@ -3323,20 +3323,20 @@ public class ImapStore extends Store { Collections.sort(msgSeqs); // Have to do comparisons in order because of msgSeq reductions for (long msgSeqNum : msgSeqs) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Comparing EXPUNGEd msgSeq " + msgSeq + " to " + msgSeqNum); + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Comparing EXPUNGEd msgSeq " + msgSeq + " to " + msgSeqNum); } if (msgSeqNum == msgSeq) { String uid = msgSeqUidMap.get(msgSeqNum); - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Scheduling removal of UID " + uid + " because msgSeq " + msgSeqNum + " was expunged"); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "Scheduling removal of UID " + uid + " because msgSeq " + msgSeqNum + " was expunged"); } removeMsgUids.add(uid); msgSeqUidMap.remove(msgSeqNum); } else if (msgSeqNum > msgSeq) { String uid = msgSeqUidMap.get(msgSeqNum); - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Reducing msgSeq for UID " + uid + " from " + msgSeqNum + " to " + (msgSeqNum - 1)); + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "Reducing msgSeq for UID " + uid + " from " + msgSeqNum + " to " + (msgSeqNum - 1)); } msgSeqUidMap.remove(msgSeqNum); msgSeqUidMap.put(msgSeqNum - 1, uid); @@ -3344,7 +3344,7 @@ public class ImapStore extends Store { } } } catch (Exception e) { - Log.e(K9.LOG_TAG, "Could not handle untagged FETCH for " + getLogId(), e); + Log.e(LOG_TAG, "Could not handle untagged FETCH for " + getLogId(), e); } } return messageCountDelta; @@ -3375,27 +3375,27 @@ public class ImapStore extends Store { } ImapConnection conn = mConnection; if (conn != null) { - if (K9.DEBUG) - Log.v(K9.LOG_TAG, "Closing mConnection to stop pushing for " + getLogId()); + if (K9MailLib.isDebug()) + Log.v(LOG_TAG, "Closing mConnection to stop pushing for " + getLogId()); conn.close(); } else { - Log.w(K9.LOG_TAG, "Attempt to interrupt null mConnection to stop pushing on folderPusher for " + getLogId()); + Log.w(LOG_TAG, "Attempt to interrupt null mConnection to stop pushing on folderPusher for " + getLogId()); } } @Override public void handleAsyncUntaggedResponse(ImapResponse response) { - if (K9.DEBUG) - Log.v(K9.LOG_TAG, "Got async response: " + response); + if (K9MailLib.isDebug()) + Log.v(LOG_TAG, "Got async response: " + response); if (stop.get()) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got async untagged response: " + response + ", but stop is set for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got async untagged response: " + response + ", but stop is set for " + getLogId()); try { sendDone(); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Exception while sending DONE for " + getLogId(), e); + Log.e(LOG_TAG, "Exception while sending DONE for " + getLogId(), e); } } else { if (response.mTag == null) { @@ -3405,22 +3405,22 @@ public class ImapStore extends Store { if (ImapResponseParser.equalsIgnoreCase(responseType, "EXISTS") || ImapResponseParser.equalsIgnoreCase(responseType, "EXPUNGE") || ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")) { if (!started) { - wakeLock.acquire(K9.PUSH_WAKE_LOCK_TIMEOUT); + wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT); started = true; } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got useful async untagged response: " + response + " for " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got useful async untagged response: " + response + " for " + getLogId()); try { sendDone(); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Exception while sending DONE for " + getLogId(), e); + Log.e(LOG_TAG, "Exception while sending DONE for " + getLogId(), e); } } } else if (response.mCommandContinuationRequested) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Idling " + getLogId()); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Idling " + getLogId()); wakeLock.release(); } @@ -3468,7 +3468,7 @@ public class ImapStore extends Store { try { folderPusher.refresh(); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Got exception while refreshing for " + folderPusher.getName(), e); + Log.e(LOG_TAG, "Got exception while refreshing for " + folderPusher.getName(), e); } } } @@ -3476,17 +3476,17 @@ public class ImapStore extends Store { @Override public void stop() { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Requested stop of IMAP pusher"); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Requested stop of IMAP pusher"); synchronized (folderPushers) { for (ImapFolderPusher folderPusher : folderPushers.values()) { try { - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "Requesting stop of IMAP folderPusher " + folderPusher.getName()); + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Requesting stop of IMAP folderPusher " + folderPusher.getName()); folderPusher.stop(); } catch (Exception e) { - Log.e(K9.LOG_TAG, "Got exception while stopping " + folderPusher.getName(), e); + Log.e(LOG_TAG, "Got exception while stopping " + folderPusher.getName(), e); } } folderPushers.clear(); @@ -3495,7 +3495,7 @@ public class ImapStore extends Store { @Override public int getRefreshInterval() { - return (getAccount().getIdleRefreshMinutes() * 60 * 1000); + return (mStoreConfig.getIdleRefreshMinutes() * 60 * 1000); } @Override @@ -3532,7 +3532,7 @@ public class ImapStore extends Store { try { newUidNext = Long.parseLong(value); } catch (NumberFormatException e) { - Log.e(K9.LOG_TAG, "Unable to part uidNext value " + value, e); + Log.e(LOG_TAG, "Unable to part uidNext value " + value, e); } } @@ -3601,4 +3601,11 @@ public class ImapStore extends Store { return null; } } + + private static String combine(Object[] parts, char separator) { + if (parts == null) { + return null; + } + return TextUtils.join(String.valueOf(separator), parts); + } } diff --git a/src/com/fsck/k9/mail/store/imap/ImapUtility.java b/src/com/fsck/k9/mail/store/ImapUtility.java similarity index 93% rename from src/com/fsck/k9/mail/store/imap/ImapUtility.java rename to src/com/fsck/k9/mail/store/ImapUtility.java index 51d1aa070..bd3b12a74 100644 --- a/src/com/fsck/k9/mail/store/imap/ImapUtility.java +++ b/src/com/fsck/k9/mail/store/ImapUtility.java @@ -15,19 +15,19 @@ * limitations under the License. */ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store; import android.util.Log; -import com.fsck.k9.K9; - import java.util.ArrayList; import java.util.List; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + /** * Utility methods for use with IMAP. */ -public class ImapUtility { +class ImapUtility { /** * Gets all of the values in a sequence set per RFC 3501. * @@ -101,12 +101,12 @@ public class ImapUtility { } } } else { - Log.d(K9.LOG_TAG, "Invalid range: " + range); + Log.d(LOG_TAG, "Invalid range: " + range); } } } } catch (NumberFormatException e) { - Log.d(K9.LOG_TAG, "Invalid range value: " + range, e); + Log.d(LOG_TAG, "Invalid range value: " + range, e); } return list; @@ -122,7 +122,7 @@ public class ImapUtility { // do nothing } - Log.d(K9.LOG_TAG, "Invalid UID value: " + number); + Log.d(LOG_TAG, "Invalid UID value: " + number); return false; } diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 5e2ee632a..73366e4ae 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -3,17 +3,14 @@ package com.fsck.k9.mail.store; import android.util.Log; -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; import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.internet.MimeMessage; -import com.fsck.k9.net.ssl.TrustedSocketFactory; +import com.fsck.k9.mail.ssl.TrustedSocketFactory; +import com.fsck.k9.mail.MessageRetrievalListener; import javax.net.ssl.SSLException; @@ -24,7 +21,6 @@ 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; @@ -35,7 +31,10 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -public class Pop3Store extends Store { +import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + +public class Pop3Store extends RemoteStore { public static final String STORE_TYPE = "POP3"; private static final String STLS_COMMAND = "STLS"; @@ -128,12 +127,12 @@ public class Pop3Store extends Store { passwordIndex++; authType = AuthType.valueOf(userInfoParts[0]); } - username = UrlEncodingHelper.decodeUtf8(userInfoParts[userIndex]); + username = decodeUtf8(userInfoParts[userIndex]); if (userInfoParts.length > passwordIndex) { if (authType == AuthType.EXTERNAL) { - clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[passwordIndex]); + clientCertificateAlias = decodeUtf8(userInfoParts[passwordIndex]); } else { - password = UrlEncodingHelper.decodeUtf8(userInfoParts[passwordIndex]); + password = decodeUtf8(userInfoParts[passwordIndex]); } } } @@ -154,11 +153,11 @@ public class Pop3Store extends Store { * @see Pop3Store#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String userEnc = encodeUtf8(server.username); String passwordEnc = (server.password != null) ? - UrlEncodingHelper.encodeUtf8(server.password) : ""; + encodeUtf8(server.password) : ""; String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; + encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -209,12 +208,12 @@ public class Pop3Store extends Store { private boolean mTopNotSupported; - public Pop3Store(Account account) throws MessagingException { - super(account); + public Pop3Store(StoreConfig storeConfig) throws MessagingException { + super(storeConfig); ServerSettings settings; try { - settings = decodeUri(mAccount.getStoreUri()); + settings = decodeUri(storeConfig.getStoreUri()); } catch (IllegalArgumentException e) { throw new MessagingException("Error while decoding store URI", e); } @@ -243,13 +242,13 @@ public class Pop3Store extends Store { @Override public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { List folders = new LinkedList(); - folders.add(getFolder(mAccount.getInboxFolderName())); + folders.add(getFolder(mStoreConfig.getInboxFolderName())); return folders; } @Override public void checkSettings() throws MessagingException { - Pop3Folder folder = new Pop3Folder(mAccount.getInboxFolderName()); + Pop3Folder folder = new Pop3Folder(mStoreConfig.getInboxFolderName()); folder.open(Folder.OPEN_MODE_RW); if (!mCapabilities.uidl) { /* @@ -272,7 +271,7 @@ public class Pop3Store extends Store { return false; } - class Pop3Folder extends Folder { + class Pop3Folder extends Folder { private Socket mSocket; private InputStream mIn; private OutputStream mOut; @@ -283,11 +282,11 @@ public class Pop3Store extends Store { private int mMessageCount; public Pop3Folder(String name) { - super(Pop3Store.this.mAccount); + super(); this.mName = name; - if (mName.equalsIgnoreCase(mAccount.getInboxFolderName())) { - mName = mAccount.getInboxFolderName(); + if (mName.equalsIgnoreCase(mStoreConfig.getInboxFolderName())) { + mName = mStoreConfig.getInboxFolderName(); } } @@ -297,7 +296,7 @@ public class Pop3Store extends Store { return; } - if (!mName.equalsIgnoreCase(mAccount.getInboxFolderName())) { + if (!mName.equalsIgnoreCase(mStoreConfig.getInboxFolderName())) { throw new MessagingException("Folder does not exist"); } @@ -313,7 +312,7 @@ public class Pop3Store extends Store { mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); - mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); if (!isOpen()) { throw new MessagingException("Unable to connect socket"); } @@ -328,7 +327,7 @@ public class Pop3Store extends Store { mSocket = TrustedSocketFactory.createSocket(mSocket, mHost, mPort, mClientCertificateAlias); - mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); if (!isOpen()) { @@ -461,7 +460,7 @@ public class Pop3Store extends Store { try { executeSimpleCommand( String.format("AUTH EXTERNAL %s", - Utility.base64Encode(mUsername)), false); + Base64.encode(mUsername)), false); } catch (Pop3ErrorResponse e) { /* * Provide notification to the user of a problem authenticating @@ -541,7 +540,7 @@ public class Pop3Store extends Store { @Override public boolean exists() throws MessagingException { - return mName.equalsIgnoreCase(mAccount.getInboxFolderName()); + return mName.equalsIgnoreCase(mStoreConfig.getInboxFolderName()); } @Override @@ -559,7 +558,7 @@ public class Pop3Store extends Store { } @Override - public Message getMessage(String uid) throws MessagingException { + public Pop3Message getMessage(String uid) throws MessagingException { Pop3Message message = mUidToMsgMap.get(uid); if (message == null) { message = new Pop3Message(uid, this); @@ -635,7 +634,7 @@ public class Pop3Store extends Store { // response = "+OK msgNum msgUid" String[] uidParts = response.split(" +"); if (uidParts.length < 3 || !"+OK".equals(uidParts[0])) { - Log.e(K9.LOG_TAG, "ERR response: " + response); + Log.e(LOG_TAG, "ERR response: " + response); return; } String msgUid = uidParts[2]; @@ -693,8 +692,8 @@ public class Pop3Store extends Store { Set unindexedUids = new HashSet(); for (String uid : uids) { if (mUidToMsgMap.get(uid) == null) { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - Log.d(K9.LOG_TAG, "Need to index UID " + uid); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + Log.d(LOG_TAG, "Need to index UID " + uid); } unindexedUids.add(uid); } @@ -719,8 +718,8 @@ public class Pop3Store extends Store { Integer msgNum = Integer.valueOf(uidParts[0]); String msgUid = uidParts[1]; if (unindexedUids.contains(msgUid)) { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - Log.d(K9.LOG_TAG, "Got msgNum " + msgNum + " for UID " + msgUid); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + Log.d(LOG_TAG, "Got msgNum " + msgNum + " for UID " + msgUid); } Pop3Message message = mUidToMsgMap.get(msgUid); @@ -734,8 +733,8 @@ public class Pop3Store extends Store { } private void indexMessage(int msgNum, Pop3Message message) { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - Log.d(K9.LOG_TAG, "Adding index for UID " + message.getUid() + " to msgNum " + msgNum); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + Log.d(LOG_TAG, "Adding index for UID " + message.getUid() + " to msgNum " + msgNum); } mMsgNumToMsgMap.put(msgNum, message); mUidToMsgMap.put(message.getUid(), message); @@ -761,7 +760,7 @@ public class Pop3Store extends Store { * @throws MessagingException */ @Override - public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { if (messages == null || messages.isEmpty()) { return; @@ -805,9 +804,9 @@ public class Pop3Store extends Store { * To convert the suggested download size we take the size * divided by the maximum line size (76). */ - if (mAccount.getMaximumAutoDownloadMessageSize() > 0) { + if (mStoreConfig.getMaximumAutoDownloadMessageSize() > 0) { fetchBody(pop3Message, - (mAccount.getMaximumAutoDownloadMessageSize() / 76)); + (mStoreConfig.getMaximumAutoDownloadMessageSize() / 76)); } else { fetchBody(pop3Message, -1); } @@ -819,7 +818,7 @@ public class Pop3Store extends Store { pop3Message.setBody(null); } if (listener != null && !(fp.contains(FetchProfile.Item.ENVELOPE) && fp.size() == 1)) { - listener.messageFinished(message, i, count); + listener.messageFinished(pop3Message, i, count); } } catch (IOException ioe) { throw new MessagingException("Unable to fetch message", ioe); @@ -906,8 +905,8 @@ public class Pop3Store extends Store { // Try hard to use the TOP command if we're not asked to download the whole message. if (lines != -1 && (!mTopNotSupported || mCapabilities.top)) { try { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3 && !mCapabilities.top) { - Log.d(K9.LOG_TAG, "This server doesn't support the CAPA command. " + + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3 && !mCapabilities.top) { + Log.d(LOG_TAG, "This server doesn't support the CAPA command. " + "Checking to see if the TOP command is supported nevertheless."); } @@ -921,8 +920,8 @@ public class Pop3Store extends Store { // The TOP command should be supported but something went wrong. throw e; } else { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - Log.d(K9.LOG_TAG, "The server really doesn't support the TOP " + + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + Log.d(LOG_TAG, "The server really doesn't support the TOP " + "command. Using RETR instead."); } @@ -1029,8 +1028,8 @@ public class Pop3Store extends Store { } } while ((d = mIn.read()) != -1); String ret = sb.toString(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - Log.d(K9.LOG_TAG, "<<< " + ret); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + Log.d(LOG_TAG, "<<< " + ret); } return ret; } @@ -1122,12 +1121,12 @@ public class Pop3Store extends Store { open(Folder.OPEN_MODE_RW); if (command != null) { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_POP3) { - if (sensitive && !K9.DEBUG_SENSITIVE) { - Log.d(K9.LOG_TAG, ">>> " + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) { + if (sensitive && !K9MailLib.isDebugSensitive()) { + Log.d(LOG_TAG, ">>> " + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); } else { - Log.d(K9.LOG_TAG, ">>> " + command); + Log.d(LOG_TAG, ">>> " + command); } } @@ -1199,7 +1198,7 @@ public class Pop3Store extends Store { // } // catch (MessagingException me) // { -// Log.w(K9.LOG_TAG, "Could not delete non-existent message", me); +// Log.w(LOG_TAG, "Could not delete non-existent message", me); // } } } diff --git a/src/com/fsck/k9/mail/store/RemoteStore.java b/src/com/fsck/k9/mail/store/RemoteStore.java new file mode 100644 index 000000000..a1f32722e --- /dev/null +++ b/src/com/fsck/k9/mail/store/RemoteStore.java @@ -0,0 +1,120 @@ +package com.fsck.k9.mail.store; + +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Store; + +import java.util.HashMap; +import java.util.Map; + +public abstract class RemoteStore extends Store { + protected static final int SOCKET_CONNECT_TIMEOUT = 30000; + protected static final int SOCKET_READ_TIMEOUT = 60000; + + protected StoreConfig mStoreConfig; + + /** + * Remote stores indexed by Uri. + */ + private static Map sStores = new HashMap(); + + + public RemoteStore(StoreConfig storeConfig) { + mStoreConfig = storeConfig; + } + + /** + * Get an instance of a remote mail store. + */ + public synchronized static Store getInstance(StoreConfig storeConfig) throws MessagingException { + String uri = storeConfig.getStoreUri(); + + if (uri.startsWith("local")) { + throw new RuntimeException("Asked to get non-local Store object but given LocalStore URI"); + } + + Store store = sStores.get(uri); + if (store == null) { + if (uri.startsWith("imap")) { + store = new ImapStore(storeConfig); + } else if (uri.startsWith("pop3")) { + store = new Pop3Store(storeConfig); + } else if (uri.startsWith("webdav")) { + store = new WebDavStore(storeConfig); + } + + if (store != null) { + sStores.put(uri, store); + } + } + + if (store == null) { + throw new MessagingException("Unable to locate an applicable Store for " + uri); + } + + return store; + } + + /** + * Release reference to a remote mail store instance. + * + * @param storeConfig {@link com.fsck.k9.mail.store.StoreConfig} instance that is used to get the remote mail store instance. + */ + public static void removeInstance(StoreConfig storeConfig) { + String uri = storeConfig.getStoreUri(); + if (uri.startsWith("local")) { + throw new RuntimeException("Asked to get non-local Store object but given " + + "LocalStore URI"); + } + sStores.remove(uri); + } + + /** + * Decodes the contents of store-specific URIs and puts them into a {@link com.fsck.k9.mail.ServerSettings} + * object. + * + * @param uri + * the store-specific URI to decode + * + * @return A {@link com.fsck.k9.mail.ServerSettings} object holding the settings contained in the URI. + * + * @see com.fsck.k9.mail.store.ImapStore#decodeUri(String) + * @see com.fsck.k9.mail.store.Pop3Store#decodeUri(String) + * @see com.fsck.k9.mail.store.WebDavStore#decodeUri(String) + */ + public static ServerSettings decodeStoreUri(String uri) { + if (uri.startsWith("imap")) { + return ImapStore.decodeUri(uri); + } else if (uri.startsWith("pop3")) { + return Pop3Store.decodeUri(uri); + } else if (uri.startsWith("webdav")) { + return WebDavStore.decodeUri(uri); + } else { + throw new IllegalArgumentException("Not a valid store URI"); + } + } + + /** + * Creates a store URI from the information supplied in the {@link com.fsck.k9.mail.ServerSettings} object. + * + * @param server + * The {@link com.fsck.k9.mail.ServerSettings} object that holds the server settings. + * + * @return A store URI that holds the same information as the {@code server} parameter. + * + * @see com.fsck.k9.mail.store.ImapStore#createUri(com.fsck.k9.mail.ServerSettings) + * @see com.fsck.k9.mail.store.Pop3Store#createUri(com.fsck.k9.mail.ServerSettings) + * @see com.fsck.k9.mail.store.WebDavStore#createUri(com.fsck.k9.mail.ServerSettings) + */ + public static String createStoreUri(ServerSettings server) { + if (ImapStore.STORE_TYPE.equals(server.type)) { + return ImapStore.createUri(server); + } else if (Pop3Store.STORE_TYPE.equals(server.type)) { + return Pop3Store.createUri(server); + } else if (WebDavStore.STORE_TYPE.equals(server.type)) { + return WebDavStore.createUri(server); + } else { + throw new IllegalArgumentException("Not a valid store URI"); + } + } +} diff --git a/src/com/fsck/k9/mail/store/StoreConfig.java b/src/com/fsck/k9/mail/store/StoreConfig.java new file mode 100644 index 000000000..d8531d978 --- /dev/null +++ b/src/com/fsck/k9/mail/store/StoreConfig.java @@ -0,0 +1,31 @@ +package com.fsck.k9.mail.store; + +public interface StoreConfig { + String getStoreUri(); + String getTransportUri(); + + boolean subscribedFoldersOnly(); + boolean useCompression(int type); + + String getInboxFolderName(); + String getOutboxFolderName(); + String getDraftsFolderName(); + + void setDraftsFolderName(String name); + void setTrashFolderName(String name); + void setSpamFolderName(String name); + void setSentFolderName(String name); + void setAutoExpandFolderName(String name); + void setInboxFolderName(String name); + + int getMaximumAutoDownloadMessageSize(); + + boolean allowRemoteSearch(); + boolean isRemoteSearchFullText(); + + boolean isPushPollOnConnect(); + + int getDisplayCount(); + + int getIdleRefreshMinutes(); +} diff --git a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java index 3557002a2..5345b41fd 100644 --- a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java +++ b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java @@ -4,7 +4,7 @@ import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.conn.scheme.LayeredSocketFactory; import org.apache.http.params.HttpParams; -import com.fsck.k9.net.ssl.TrustManagerFactory; +import com.fsck.k9.mail.ssl.TrustManagerFactory; import java.io.IOException; import java.net.InetAddress; diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 27d0c7ccb..9944c6a9e 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -2,15 +2,11 @@ package com.fsck.k9.mail.store; 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.Base64; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.MessageRetrievalListener; import org.apache.commons.io.IOUtils; import org.apache.http.*; @@ -49,13 +45,16 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.zip.GZIPInputStream; +import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + /** *
      * Uses WebDAV formatted HTTP calls to an MS Exchange server to fetch email
      * and email information.
      * 
    */ -public class WebDavStore extends Store { +public class WebDavStore extends RemoteStore { public static final String STORE_TYPE = "WebDAV"; // Authentication types @@ -138,7 +137,7 @@ public class WebDavStore extends Store { String userInfo = webDavUri.getUserInfo(); if (userInfo != null) { String[] userInfoParts = userInfo.split(":"); - username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + username = decodeUtf8(userInfoParts[0]); String userParts[] = username.split("\\\\", 2); if (userParts.length > 1) { @@ -147,7 +146,7 @@ public class WebDavStore extends Store { alias = username; } if (userInfoParts.length > 1) { - password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + password = decodeUtf8(userInfoParts[1]); } } @@ -187,9 +186,9 @@ public class WebDavStore extends Store { * @see WebDavStore#decodeUri(String) */ public static String createUri(ServerSettings server) { - String userEnc = UrlEncodingHelper.encodeUtf8(server.username); + String userEnc = encodeUtf8(server.username); String passwordEnc = (server.password != null) ? - UrlEncodingHelper.encodeUtf8(server.password) : ""; + encodeUtf8(server.password) : ""; String scheme; switch (server.connectionSecurity) { @@ -294,12 +293,12 @@ public class WebDavStore extends Store { private Map mFolderList = new HashMap(); - public WebDavStore(Account account) throws MessagingException { - super(account); + public WebDavStore(StoreConfig storeConfig) throws MessagingException { + super(storeConfig); WebDavStoreSettings settings; try { - settings = decodeUri(mAccount.getStoreUri()); + settings = decodeUri(storeConfig.getStoreUri()); } catch (IllegalArgumentException e) { throw new MessagingException("Error while decoding store URI", e); } @@ -340,7 +339,7 @@ public class WebDavStore extends Store { // The inbox path would look like: "https://mail.domain.com/Exchange/alias/Inbox". mUrl = getRoot() + mPath + mMailboxPath; - mAuthString = "Basic " + Utility.base64Encode(mUsername + ":" + mPassword); + mAuthString = "Basic " + Base64.encode(mUsername + ":" + mPassword); } private String getRoot() { @@ -380,21 +379,21 @@ public class WebDavStore extends Store { Map specialFoldersMap = dataset.getSpecialFolderToUrl(); String folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_INBOX_FOLDER)); if (folderName != null) { - mAccount.setAutoExpandFolderName(folderName); - mAccount.setInboxFolderName(folderName); + mStoreConfig.setAutoExpandFolderName(folderName); + mStoreConfig.setInboxFolderName(folderName); } folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_DRAFTS_FOLDER)); if (folderName != null) - mAccount.setDraftsFolderName(folderName); + mStoreConfig.setDraftsFolderName(folderName); folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_TRASH_FOLDER)); if (folderName != null) - mAccount.setTrashFolderName(folderName); + mStoreConfig.setTrashFolderName(folderName); folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_SPAM_FOLDER)); if (folderName != null) - mAccount.setSpamFolderName(folderName); + mStoreConfig.setSpamFolderName(folderName); // K-9 Mail's outbox is a special local folder and different from Exchange/WebDAV's outbox. /* @@ -405,7 +404,7 @@ public class WebDavStore extends Store { folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_SENT_FOLDER)); if (folderName != null) - mAccount.setSentFolderName(folderName); + mStoreConfig.setSentFolderName(folderName); /** * Next we get all the folders (including "special" ones) @@ -475,7 +474,7 @@ public class WebDavStore extends Store { // Decodes the url-encoded folder name (i.e. "My%20folder" => "My Folder" - return UrlEncodingHelper.decodeUtf8(fullPathName); + return decodeUtf8(fullPathName); } return null; @@ -721,7 +720,7 @@ public class WebDavStore extends Store { doFBA(null); } } catch (IOException ioe) { - Log.e(K9.LOG_TAG, "Error during authentication: " + ioe + "\nStack: " + processException(ioe)); + Log.e(LOG_TAG, "Error during authentication: " + ioe + "\nStack: " + processException(ioe)); throw new MessagingException("Error during authentication", ioe); } @@ -791,7 +790,7 @@ public class WebDavStore extends Store { } catch (SSLException e) { throw new CertificateValidationException(e.getMessage(), e); } catch (IOException ioe) { - Log.e(K9.LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); + Log.e(LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); throw new MessagingException("IOException", ioe); } @@ -893,7 +892,7 @@ public class WebDavStore extends Store { response = httpClient.executeOverride(request, mContext); authenticated = testAuthenticationResponse(response); } catch (URISyntaxException e) { - Log.e(K9.LOG_TAG, "URISyntaxException caught " + e + "\nTrace: " + processException(e)); + Log.e(LOG_TAG, "URISyntaxException caught " + e + "\nTrace: " + processException(e)); throw new MessagingException("URISyntaxException caught", e); } } else { @@ -987,7 +986,7 @@ public class WebDavStore extends Store { } } } catch (URISyntaxException e) { - Log.e(K9.LOG_TAG, "URISyntaxException caught " + e + "\nTrace: " + processException(e)); + Log.e(LOG_TAG, "URISyntaxException caught " + e + "\nTrace: " + processException(e)); throw new MessagingException("URISyntaxException caught", e); } } @@ -1024,10 +1023,10 @@ public class WebDavStore extends Store { Scheme s = new Scheme("https", new WebDavSocketFactory(mHost, 443), 443); reg.register(s); } catch (NoSuchAlgorithmException nsa) { - Log.e(K9.LOG_TAG, "NoSuchAlgorithmException in getHttpClient: " + nsa); + Log.e(LOG_TAG, "NoSuchAlgorithmException in getHttpClient: " + nsa); throw new MessagingException("NoSuchAlgorithmException in getHttpClient: " + nsa); } catch (KeyManagementException kme) { - Log.e(K9.LOG_TAG, "KeyManagementException in getHttpClient: " + kme); + Log.e(LOG_TAG, "KeyManagementException in getHttpClient: " + kme); throw new MessagingException("KeyManagementException in getHttpClient: " + kme); } } @@ -1094,10 +1093,10 @@ public class WebDavStore extends Store { istream = WebDavHttpClient.getUngzippedContent(entity); } } catch (UnsupportedEncodingException uee) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException: " + uee + "\nTrace: " + processException(uee)); + Log.e(LOG_TAG, "UnsupportedEncodingException: " + uee + "\nTrace: " + processException(uee)); throw new MessagingException("UnsupportedEncodingException", uee); } catch (IOException ioe) { - Log.e(K9.LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); + Log.e(LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); throw new MessagingException("IOException", ioe); } @@ -1122,8 +1121,8 @@ public class WebDavStore extends Store { boolean needsParsing) throws MessagingException { DataSet dataset = new DataSet(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_WEBDAV) { - Log.v(K9.LOG_TAG, "processRequest url = '" + url + "', method = '" + method + "', messageBody = '" + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) { + Log.v(LOG_TAG, "processRequest url = '" + url + "', method = '" + method + "', messageBody = '" + messageBody + "'"); } @@ -1155,10 +1154,10 @@ public class WebDavStore extends Store { dataset = myHandler.getDataSet(); } catch (SAXException se) { - Log.e(K9.LOG_TAG, "SAXException in processRequest() " + se + "\nTrace: " + processException(se)); + Log.e(LOG_TAG, "SAXException in processRequest() " + se + "\nTrace: " + processException(se)); throw new MessagingException("SAXException in processRequest() ", se); } catch (ParserConfigurationException pce) { - Log.e(K9.LOG_TAG, "ParserConfigurationException in processRequest() " + pce + "\nTrace: " + Log.e(LOG_TAG, "ParserConfigurationException in processRequest() " + pce + "\nTrace: " + processException(pce)); throw new MessagingException("ParserConfigurationException in processRequest() ", pce); } @@ -1166,10 +1165,10 @@ public class WebDavStore extends Store { istream.close(); } } catch (UnsupportedEncodingException uee) { - Log.e(K9.LOG_TAG, "UnsupportedEncodingException: " + uee + "\nTrace: " + processException(uee)); + Log.e(LOG_TAG, "UnsupportedEncodingException: " + uee + "\nTrace: " + processException(uee)); throw new MessagingException("UnsupportedEncodingException in processRequest() ", uee); } catch (IOException ioe) { - Log.e(K9.LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); + Log.e(LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe)); throw new MessagingException("IOException in processRequest() ", ioe); } @@ -1196,7 +1195,7 @@ public class WebDavStore extends Store { @Override public void sendMessages(List messages) throws MessagingException { - WebDavFolder tmpFolder = (WebDavStore.WebDavFolder) getFolder(mAccount.getDraftsFolderName()); + WebDavFolder tmpFolder = (WebDavStore.WebDavFolder) getFolder(mStoreConfig.getDraftsFolderName()); try { tmpFolder.open(Folder.OPEN_MODE_RW); List retMessages = tmpFolder.appendWebDavMessages(messages); @@ -1216,7 +1215,7 @@ public class WebDavStore extends Store { /** * A WebDav Folder */ - class WebDavFolder extends Folder { + class WebDavFolder extends Folder { private String mName; private String mFolderUrl; private boolean mIsOpen = false; @@ -1229,7 +1228,7 @@ public class WebDavStore extends Store { } public WebDavFolder(WebDavStore nStore, String name) { - super(nStore.getAccount()); + super(); store = nStore; this.mName = name; @@ -1238,9 +1237,9 @@ public class WebDavStore extends Store { String url = ""; for (int i = 0, count = urlParts.length; i < count; i++) { if (i != 0) { - url = url + "/" + UrlEncodingHelper.encodeUtf8(urlParts[i]); + url = url + "/" + encodeUtf8(urlParts[i]); } else { - url = UrlEncodingHelper.encodeUtf8(urlParts[i]); + url = encodeUtf8(urlParts[i]); } } encodedName = url; @@ -1310,7 +1309,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.size() + " messages to " + destFolder.mFolderUrl); + Log.i(LOG_TAG, "Moving " + messages.size() + " messages to " + destFolder.mFolderUrl); processRequest(mFolderUrl, action, messageBody, headers, false); } @@ -1333,8 +1332,8 @@ public class WebDavStore extends Store { if (dataset != null) { messageCount = dataset.getMessageCount(); } - if (K9.DEBUG && K9.DEBUG_PROTOCOL_WEBDAV) { - Log.v(K9.LOG_TAG, "Counted messages and webdav returned: "+messageCount); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) { + Log.v(LOG_TAG, "Counted messages and webdav returned: "+messageCount); } return messageCount; @@ -1397,7 +1396,7 @@ public class WebDavStore extends Store { } @Override - public Message getMessage(String uid) throws MessagingException { + public WebDavMessage getMessage(String uid) throws MessagingException { return new WebDavMessage(uid, this); } @@ -1498,7 +1497,7 @@ public class WebDavStore extends Store { } @Override - public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(List messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { if (messages == null || messages.isEmpty()) { @@ -1519,8 +1518,8 @@ public class WebDavStore extends Store { } if (fp.contains(FetchProfile.Item.BODY_SANE)) { - if (mAccount.getMaximumAutoDownloadMessageSize() > 0) { - fetchMessages(messages, listener, (mAccount.getMaximumAutoDownloadMessageSize() / 76)); + if (mStoreConfig.getMaximumAutoDownloadMessageSize() > 0) { + fetchMessages(messages, listener, (mStoreConfig.getMaximumAutoDownloadMessageSize() / 76)); } else { fetchMessages(messages, listener, -1); } @@ -1533,7 +1532,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(List messages, MessageRetrievalListener listener, int lines) + private void fetchMessages(List messages, MessageRetrievalListener listener, int lines) throws MessagingException { WebDavHttpClient httpclient; httpclient = getHttpClient(); @@ -1561,7 +1560,7 @@ public class WebDavStore extends Store { */ if (wdMessage.getUrl().equals("")) { wdMessage.setUrl(getMessageUrls(new String[] { wdMessage.getUid() }).get(wdMessage.getUid())); - Log.i(K9.LOG_TAG, "Fetching messages with UID = '" + wdMessage.getUid() + "', URL = '" + Log.i(LOG_TAG, "Fetching messages with UID = '" + wdMessage.getUid() + "', URL = '" + wdMessage.getUrl() + "'"); if (wdMessage.getUrl().equals("")) { throw new MessagingException("Unable to get URL for message"); @@ -1569,7 +1568,7 @@ public class WebDavStore extends Store { } try { - Log.i(K9.LOG_TAG, "Fetching message with UID = '" + wdMessage.getUid() + "', URL = '" + Log.i(LOG_TAG, "Fetching message with UID = '" + wdMessage.getUid() + "', URL = '" + wdMessage.getUrl() + "'"); HttpGet httpget = new HttpGet(new URI(wdMessage.getUrl())); HttpResponse response; @@ -1594,8 +1593,8 @@ public class WebDavStore extends Store { if (entity != null) { InputStream istream = null; StringBuilder buffer = new StringBuilder(); - String tempText = ""; - String resultText = ""; + String tempText; + String resultText; BufferedReader reader = null; int currentLines = 0; @@ -1625,13 +1624,13 @@ public class WebDavStore extends Store { } } catch (IllegalArgumentException iae) { - Log.e(K9.LOG_TAG, "IllegalArgumentException caught " + iae + "\nTrace: " + processException(iae)); + Log.e(LOG_TAG, "IllegalArgumentException caught " + iae + "\nTrace: " + processException(iae)); throw new MessagingException("IllegalArgumentException caught", iae); } catch (URISyntaxException use) { - Log.e(K9.LOG_TAG, "URISyntaxException caught " + use + "\nTrace: " + processException(use)); + Log.e(LOG_TAG, "URISyntaxException caught " + use + "\nTrace: " + processException(use)); throw new MessagingException("URISyntaxException caught", use); } catch (IOException ioe) { - Log.e(K9.LOG_TAG, "Non-success response code loading message, response code was " + statusCode + Log.e(LOG_TAG, "Non-success response code loading message, response code was " + statusCode + "\nURL: " + wdMessage.getUrl() + "\nError: " + ioe.getMessage() + "\nTrace: " + processException(ioe)); throw new MessagingException("Failure code " + statusCode, ioe); @@ -1702,7 +1701,7 @@ public class WebDavStore extends Store { try { wdMessage.setFlagInternal(Flag.SEEN, uidToReadStatus.get(wdMessage.getUid())); } catch (NullPointerException e) { - Log.v(K9.LOG_TAG,"Under some weird circumstances, setting the read status when syncing from webdav threw an NPE. Skipping."); + Log.v(LOG_TAG,"Under some weird circumstances, setting the read status when syncing from webdav threw an NPE. Skipping."); } if (listener != null) { @@ -1771,7 +1770,7 @@ public class WebDavStore extends Store { wdMessage.setNewHeaders(envelope); wdMessage.setFlagInternal(Flag.SEEN, envelope.getReadStatus()); } else { - Log.e(K9.LOG_TAG,"Asked to get metadata for a non-existent message: "+wdMessage.getUid()); + Log.e(LOG_TAG,"Asked to get metadata for a non-existent message: "+wdMessage.getUid()); } if (listener != null) { @@ -1881,9 +1880,9 @@ public class WebDavStore extends Store { if (!messageURL.endsWith("/")) { messageURL += "/"; } - messageURL += UrlEncodingHelper.encodeUtf8(message.getUid() + ":" + System.currentTimeMillis() + ".eml"); + messageURL += encodeUtf8(message.getUid() + ":" + System.currentTimeMillis() + ".eml"); - Log.i(K9.LOG_TAG, "Uploading message as " + messageURL); + Log.i(LOG_TAG, "Uploading message as " + messageURL); httpmethod = new HttpGeneric(messageURL); httpmethod.setMethod("PUT"); @@ -1922,7 +1921,7 @@ public class WebDavStore extends Store { @Override public String getUidFromMessageId(Message message) throws MessagingException { - Log.e(K9.LOG_TAG, + Log.e(LOG_TAG, "Unimplemented method getUidFromMessageId in WebDavStore.WebDavFolder could lead to duplicate messages " + " being uploaded to the Sent folder"); return null; @@ -1930,7 +1929,7 @@ public class WebDavStore extends Store { @Override public void setFlags(final Set flags, boolean value) throws MessagingException { - Log.e(K9.LOG_TAG, + Log.e(LOG_TAG, "Unimplemented method setFlags(Set, boolean) breaks markAllMessagesAsRead and EmptyTrash"); // Try to make this efficient by not retrieving all of the messages } @@ -1968,11 +1967,11 @@ public class WebDavStore extends Store { * We have to decode, then encode the URL because Exchange likes to not properly encode all characters */ try { - end = UrlEncodingHelper.decodeUtf8(end); - end = UrlEncodingHelper.encodeUtf8(end); + end = decodeUtf8(end); + end = encodeUtf8(end); end = end.replaceAll("\\+", "%20"); } catch (IllegalArgumentException iae) { - Log.e(K9.LOG_TAG, "IllegalArgumentException caught in setUrl: " + iae + "\nTrace: " + Log.e(LOG_TAG, "IllegalArgumentException caught in setUrl: " + iae + "\nTrace: " + processException(iae)); } @@ -2022,7 +2021,7 @@ public class WebDavStore extends Store { @Override public void delete(String trashFolderName) throws MessagingException { WebDavFolder wdFolder = (WebDavFolder) getFolder(); - Log.i(K9.LOG_TAG, "Deleting message by moving to " + trashFolderName); + Log.i(LOG_TAG, "Deleting message by moving to " + trashFolderName); wdFolder.moveMessages(Collections.singletonList(this), wdFolder.getStore().getFolder(trashFolderName)); } @@ -2320,7 +2319,7 @@ public class WebDavStore extends Store { Date parsedDate = dfInput.parse(date); tempDate = dfOutput.format(parsedDate); } catch (java.text.ParseException pe) { - Log.e(K9.LOG_TAG, "Error parsing date: " + pe + "\nTrace: " + processException(pe)); + Log.e(LOG_TAG, "Error parsing date: " + pe + "\nTrace: " + processException(pe)); } envelope.addHeader(header, tempDate); } else { @@ -2359,8 +2358,8 @@ public class WebDavStore extends Store { public HttpGeneric(final String uri) { super(); - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Starting uri = '" + uri + "'"); + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Starting uri = '" + uri + "'"); } String[] urlParts = uri.split("/"); @@ -2373,12 +2372,12 @@ public class WebDavStore extends Store { */ try { if (length > 3) { - end = UrlEncodingHelper.decodeUtf8(end); - end = UrlEncodingHelper.encodeUtf8(end); + end = decodeUtf8(end); + end = encodeUtf8(end); end = end.replaceAll("\\+", "%20"); } } catch (IllegalArgumentException iae) { - Log.e(K9.LOG_TAG, "IllegalArgumentException caught in HttpGeneric(String uri): " + iae + "\nTrace: " + Log.e(LOG_TAG, "IllegalArgumentException caught in HttpGeneric(String uri): " + iae + "\nTrace: " + processException(iae)); } @@ -2389,13 +2388,13 @@ public class WebDavStore extends Store { url = urlParts[i]; } } - if (K9.DEBUG && K9.DEBUG_PROTOCOL_WEBDAV) { - Log.v(K9.LOG_TAG, "url = '" + url + "' length = " + url.length() + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) { + Log.v(LOG_TAG, "url = '" + url + "' length = " + url.length() + ", end = '" + end + "' length = " + end.length()); } url = url + "/" + end; - Log.i(K9.LOG_TAG, "url = " + url); + Log.i(LOG_TAG, "url = " + url); setURI(URI.create(url)); } @@ -2425,7 +2424,7 @@ public class WebDavStore extends Store { * the License for the specific language governing permissions and limitations under the License. */ public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { - Log.i(K9.LOG_TAG, "Requesting gzipped data"); + Log.i(LOG_TAG, "Requesting gzipped data"); request.addHeader("Accept-Encoding", "gzip"); } @@ -2441,7 +2440,7 @@ public class WebDavStore extends Store { if (contentEncoding == null) return responseStream; if (contentEncoding.contains("gzip")) { - Log.i(K9.LOG_TAG, "Response is gzipped"); + Log.i(LOG_TAG, "Response is gzipped"); responseStream = new GZIPInputStream(responseStream); } return responseStream; diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index 428c0dce2..485e45f02 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -3,20 +3,18 @@ package com.fsck.k9.mail.transport; 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; +import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.LineWrapOutputStream; import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.SmtpDataStuffing; -import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.local.LocalMessage; -import com.fsck.k9.net.ssl.TrustedSocketFactory; +import com.fsck.k9.mail.internet.CharsetSupport; +import com.fsck.k9.mail.store.StoreConfig; +import com.fsck.k9.mail.ssl.TrustedSocketFactory; import javax.net.ssl.SSLException; @@ -28,6 +26,9 @@ import java.net.*; import java.security.GeneralSecurityException; import java.util.*; +import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_SMTP; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + public class SmtpTransport extends Transport { public static final String TRANSPORT_TYPE = "SMTP"; @@ -95,19 +96,19 @@ public class SmtpTransport extends Transport { String[] userInfoParts = smtpUri.getUserInfo().split(":"); if (userInfoParts.length == 1) { authType = AuthType.PLAIN; - username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); + username = decodeUtf8(userInfoParts[0]); } else if (userInfoParts.length == 2) { authType = AuthType.PLAIN; - username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]); - password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + username = decodeUtf8(userInfoParts[0]); + password = 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]); + username = decodeUtf8(userInfoParts[0]); if (authType == AuthType.EXTERNAL) { - clientCertificateAlias = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + clientCertificateAlias = decodeUtf8(userInfoParts[1]); } else { - password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]); + password = decodeUtf8(userInfoParts[1]); } } } @@ -124,16 +125,16 @@ public class SmtpTransport extends Transport { * * @return A SmtpTransport URI that holds the same information as the {@code server} parameter. * - * @see Account#getTransportUri() + * @see com.fsck.k9.mail.store.StoreConfig#getTransportUri() * @see SmtpTransport#decodeUri(String) */ public static String createUri(ServerSettings server) { String userEnc = (server.username != null) ? - UrlEncodingHelper.encodeUtf8(server.username) : ""; + encodeUtf8(server.username) : ""; String passwordEnc = (server.password != null) ? - UrlEncodingHelper.encodeUtf8(server.password) : ""; + encodeUtf8(server.password) : ""; String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ? - UrlEncodingHelper.encodeUtf8(server.clientCertificateAlias) : ""; + encodeUtf8(server.clientCertificateAlias) : ""; String scheme; switch (server.connectionSecurity) { @@ -149,7 +150,7 @@ public class SmtpTransport extends Transport { break; } - String userInfo = null; + String userInfo; AuthType authType = server.authenticationType; // NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store! if (authType != null) { @@ -183,10 +184,10 @@ public class SmtpTransport extends Transport { private boolean m8bitEncodingAllowed; private int mLargestAcceptableMessage; - public SmtpTransport(Account account) throws MessagingException { + public SmtpTransport(StoreConfig storeConfig) throws MessagingException { ServerSettings settings; try { - settings = decodeUri(account.getTransportUri()); + settings = decodeUri(storeConfig.getTransportUri()); } catch (IllegalArgumentException e) { throw new MessagingException("Error while decoding transport URI", e); } @@ -305,8 +306,8 @@ public class SmtpTransport extends Transport { try { mLargestAcceptableMessage = Integer.parseInt(extensions.get("SIZE")); } catch (Exception e) { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { - Log.d(K9.LOG_TAG, "Tried to parse " + extensions.get("SIZE") + " and get an int", e); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { + Log.d(LOG_TAG, "Tried to parse " + extensions.get("SIZE") + " and get an int", e); } } } @@ -438,14 +439,14 @@ public class SmtpTransport extends Transport { extensions.put(pair[0].toUpperCase(Locale.US), pair.length == 1 ? "" : pair[1]); } } catch (NegativeSmtpReplyException e) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Server doesn't support the EHLO command. Trying HELO..."); + if (K9MailLib.isDebug()) { + Log.v(LOG_TAG, "Server doesn't support the EHLO command. Trying HELO..."); } try { executeSimpleCommand("HELO " + host); } catch (NegativeSmtpReplyException e2) { - Log.w(K9.LOG_TAG, "Server doesn't support the HELO command. Continuing anyway."); + Log.w(LOG_TAG, "Server doesn't support the HELO command. Continuing anyway."); } } return extensions; @@ -465,7 +466,7 @@ public class SmtpTransport extends Transport { new HashMap>(); for (Address address : addresses) { String addressString = address.getAddress(); - String charset = MimeUtility.getCharsetFromAddress(addressString); + String charset = CharsetSupport.getCharsetFromAddress(addressString); List addressesOfCharset = charsetAddressesMap.get(charset); if (addressesOfCharset == null) { addressesOfCharset = new ArrayList(); @@ -495,7 +496,7 @@ public class SmtpTransport extends Transport { } // If the message has attachments and our server has told us about a limit on // the size of messages, count the message's size before sending it - if (mLargestAcceptableMessage > 0 && ((LocalMessage)message).hasAttachments()) { + if (mLargestAcceptableMessage > 0 && 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? @@ -529,7 +530,7 @@ public class SmtpTransport extends Transport { // "5xx text" -responses are permanent failures String msg = e.getMessage(); if (msg != null && msg.startsWith("5")) { - Log.w(K9.LOG_TAG, "handling 5xx SMTP error code as a permanent failure"); + Log.w(LOG_TAG, "handling 5xx SMTP error code as a permanent failure"); possibleSend = false; } @@ -582,21 +583,21 @@ public class SmtpTransport extends Transport { } } String ret = sb.toString(); - if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) - Log.d(K9.LOG_TAG, "SMTP <<< " + ret); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) + Log.d(LOG_TAG, "SMTP <<< " + ret); return ret; } private void writeLine(String s, boolean sensitive) throws IOException { - if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { final String commandToLog; - if (sensitive && !K9.DEBUG_SENSITIVE) { + if (sensitive && !K9MailLib.isDebugSensitive()) { commandToLog = "SMTP >>> *sensitive*"; } else { commandToLog = "SMTP >>> " + s; } - Log.d(K9.LOG_TAG, commandToLog); + Log.d(LOG_TAG, commandToLog); } byte[] data = s.concat("\r\n").getBytes(); @@ -701,8 +702,8 @@ public class SmtpTransport extends Transport { AuthenticationFailedException, IOException { try { executeSimpleCommand("AUTH LOGIN"); - executeSimpleCommand(Utility.base64Encode(username), true); - executeSimpleCommand(Utility.base64Encode(password), true); + executeSimpleCommand(Base64.encode(username), true); + executeSimpleCommand(Base64.encode(password), true); } catch (NegativeSmtpReplyException exception) { if (exception.getReplyCode() == 535) { // Authentication credentials invalid @@ -716,7 +717,7 @@ public class SmtpTransport extends Transport { private void saslAuthPlain(String username, String password) throws MessagingException, AuthenticationFailedException, IOException { - String data = Utility.base64Encode("\000" + username + "\000" + password); + String data = Base64.encode("\000" + username + "\000" + password); try { executeSimpleCommand("AUTH PLAIN " + data, true); } catch (NegativeSmtpReplyException exception) { @@ -756,7 +757,7 @@ public class SmtpTransport extends Transport { private void saslAuthExternal(String username) throws MessagingException, IOException { executeSimpleCommand( String.format("AUTH EXTERNAL %s", - Utility.base64Encode(username)), false); + Base64.encode(username)), false); } /** diff --git a/src/com/fsck/k9/mail/transport/WebDavTransport.java b/src/com/fsck/k9/mail/transport/WebDavTransport.java index 1c94d01d5..2693aa8c4 100644 --- a/src/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/src/com/fsck/k9/mail/transport/WebDavTransport.java @@ -3,16 +3,18 @@ package com.fsck.k9.mail.transport; import android.util.Log; -import com.fsck.k9.Account; -import com.fsck.k9.K9; +import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mail.store.WebDavStore; import java.util.Collections; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + public class WebDavTransport extends Transport { public static final String TRANSPORT_TYPE = WebDavStore.STORE_TYPE; @@ -43,21 +45,17 @@ public class WebDavTransport extends Transport { private WebDavStore store; - public WebDavTransport(Account account) throws MessagingException { - if (account.getRemoteStore() instanceof WebDavStore) { - store = (WebDavStore) account.getRemoteStore(); - } else { - store = new WebDavStore(account); - } + public WebDavTransport(StoreConfig storeConfig) throws MessagingException { + store = new WebDavStore(storeConfig); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, ">>> New WebDavTransport creation complete"); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, ">>> New WebDavTransport creation complete"); } @Override public void open() throws MessagingException { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, ">>> open called on WebDavTransport "); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, ">>> open called on WebDavTransport "); store.getHttpClient(); } diff --git a/src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java b/src/com/fsck/k9/mailstore/AttachmentMessageBodyUtil.java similarity index 96% rename from src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java rename to src/com/fsck/k9/mailstore/AttachmentMessageBodyUtil.java index a1f4d5b3b..71fc418d6 100644 --- a/src/com/fsck/k9/mail/store/local/AttachmentMessageBodyUtil.java +++ b/src/com/fsck/k9/mailstore/AttachmentMessageBodyUtil.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.IOException; import java.io.InputStream; diff --git a/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java b/src/com/fsck/k9/mailstore/BinaryAttachmentBody.java similarity index 97% rename from src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java rename to src/com/fsck/k9/mailstore/BinaryAttachmentBody.java index 8efcf8992..ecf40c75f 100644 --- a/src/com/fsck/k9/mail/store/local/BinaryAttachmentBody.java +++ b/src/com/fsck/k9/mailstore/BinaryAttachmentBody.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.IOException; import java.io.InputStream; diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java b/src/com/fsck/k9/mailstore/LocalAttachmentBody.java similarity index 96% rename from src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java rename to src/com/fsck/k9/mailstore/LocalAttachmentBody.java index 7efdf949c..0accef810 100644 --- a/src/com/fsck/k9/mail/store/local/LocalAttachmentBody.java +++ b/src/com/fsck/k9/mailstore/LocalAttachmentBody.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java b/src/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java similarity index 95% rename from src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java rename to src/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java index e72d3b493..ab6eae968 100644 --- a/src/com/fsck/k9/mail/store/local/LocalAttachmentBodyPart.java +++ b/src/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; diff --git a/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java b/src/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java similarity index 97% rename from src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java rename to src/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java index d953bfc73..bd0a4329f 100644 --- a/src/com/fsck/k9/mail/store/local/LocalAttachmentMessageBody.java +++ b/src/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.IOException; import java.io.OutputStream; diff --git a/src/com/fsck/k9/mail/store/local/LocalFolder.java b/src/com/fsck/k9/mailstore/LocalFolder.java similarity index 97% rename from src/com/fsck/k9/mail/store/local/LocalFolder.java rename to src/com/fsck/k9/mailstore/LocalFolder.java index 56e2558e4..63e958ec9 100644 --- a/src/com/fsck/k9/mail/store/local/LocalFolder.java +++ b/src/com/fsck/k9/mailstore/LocalFolder.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.File; import java.io.FileOutputStream; @@ -28,11 +28,11 @@ 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.Account.MessageFormat; import com.fsck.k9.activity.Search; -import com.fsck.k9.controller.MessageRemovalListener; -import com.fsck.k9.controller.MessageRetrievalListener; +import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; @@ -51,14 +51,12 @@ 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.local.LockableDatabase.DbCallback; -import com.fsck.k9.mail.store.local.LockableDatabase.WrappedException; +import com.fsck.k9.mailstore.LockableDatabase.DbCallback; +import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.AttachmentProvider; -public class LocalFolder extends Folder implements Serializable { + +public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; @@ -80,22 +78,19 @@ public class LocalFolder extends Folder implements Serializable { private Integer mLastUid = null; public LocalFolder(LocalStore localStore, String name) { - super(localStore.getAccount()); + super(); this.localStore = localStore; this.mName = name; - if (this.localStore.getAccount().getInboxFolderName().equals(getName())) { - + if (getAccount().getInboxFolderName().equals(getName())) { mSyncClass = FolderClass.FIRST_CLASS; mPushClass = FolderClass.FIRST_CLASS; mInTopGroup = true; } - - } public LocalFolder(LocalStore localStore, long id) { - super(localStore.getAccount()); + super(); this.localStore = localStore; this.mFolderId = id; } @@ -104,6 +99,23 @@ public class LocalFolder extends Folder implements Serializable { return mFolderId; } + public String getAccountUuid() + { + return getAccount().getUuid(); + } + + public boolean getSignatureUse() { + return getAccount().getSignatureUse(); + } + + public void setLastSelectedFolderName(String destFolderName) { + getAccount().setLastSelectedFolderName(destFolderName); + } + + public boolean syncRemoteDeletions() { + return getAccount().syncRemoteDeletions(); + } + @Override public void open(final int mode) throws MessagingException { @@ -216,7 +228,7 @@ public class LocalFolder extends Folder implements Serializable { @Override public boolean create(FolderType type) throws MessagingException { - return create(type, mAccount.getDisplayCount()); + return create(type, getAccount().getDisplayCount()); } @Override @@ -517,25 +529,25 @@ public class LocalFolder extends Folder implements Serializable { 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())) { + if (mDisplayClass == FolderClass.NO_CLASS && !getAccount().getInboxFolderName().equals(getName())) { editor.remove(id + ".displayMode"); } else { editor.putString(id + ".displayMode", mDisplayClass.name()); } - if (mSyncClass == FolderClass.INHERITED && !mAccount.getInboxFolderName().equals(getName())) { + if (mSyncClass == FolderClass.INHERITED && !getAccount().getInboxFolderName().equals(getName())) { editor.remove(id + ".syncMode"); } else { editor.putString(id + ".syncMode", mSyncClass.name()); } - if (mNotifyClass == FolderClass.INHERITED && !mAccount.getInboxFolderName().equals(getName())) { + if (mNotifyClass == FolderClass.INHERITED && !getAccount().getInboxFolderName().equals(getName())) { editor.remove(id + ".notifyMode"); } else { editor.putString(id + ".notifyMode", mNotifyClass.name()); } - if (mPushClass == FolderClass.SECOND_CLASS && !mAccount.getInboxFolderName().equals(getName())) { + if (mPushClass == FolderClass.SECOND_CLASS && !getAccount().getInboxFolderName().equals(getName())) { editor.remove(id + ".pushMode"); } else { editor.putString(id + ".pushMode", mPushClass.name()); @@ -597,7 +609,7 @@ public class LocalFolder extends Folder implements Serializable { } @Override - public void fetch(final List messages, final FetchProfile fp, final MessageRetrievalListener listener) + public void fetch(final List messages, final FetchProfile fp, final MessageRetrievalListener listener) throws MessagingException { try { this.localStore.database.execute(false, new DbCallback() { @@ -614,7 +626,7 @@ public class LocalFolder extends Folder implements Serializable { try { cursor = db.rawQuery("SELECT html_content, text_content, mime_type FROM messages " + "WHERE id = ?", - new String[] { Long.toString(localMessage.mId) }); + new String[] { Long.toString(localMessage.getId()) }); cursor.moveToNext(); String htmlContent = cursor.getString(0); String textContent = cursor.getString(1); @@ -629,7 +641,7 @@ public class LocalFolder extends Folder implements Serializable { mp.addBodyPart(bp); } - if (mAccount.getMessageFormat() != MessageFormat.TEXT) { + if (getAccount().getMessageFormat() != MessageFormat.TEXT) { if (htmlContent != null) { TextBody body = new TextBody(htmlContent); MimeBodyPart bp = new MimeBodyPart(body, "text/html"); @@ -700,7 +712,7 @@ public class LocalFolder extends Folder implements Serializable { "content_disposition" }, "message_id = ?", - new String[] { Long.toString(localMessage.mId) }, + new String[] { Long.toString(localMessage.getId()) }, null, null, null); @@ -813,10 +825,10 @@ public class LocalFolder extends Folder implements Serializable { * The messages whose headers should be loaded. * @throws UnavailableStorageException */ - void populateHeaders(final List messages) throws UnavailableStorageException { + void populateHeaders(final List messages) throws MessagingException { this.localStore.database.execute(false, new DbCallback() { @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException { Cursor cursor = null; if (messages.isEmpty()) { return null; @@ -1123,14 +1135,14 @@ public class LocalFolder extends Folder implements Serializable { * @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() { + public LocalMessage 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 { + public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { try { appendMessages(Collections.singletonList(message)); final String uid = message.getUid(); - final Message result = getMessage(uid); + final LocalMessage result = getMessage(uid); runnable.run(); // Set a flag indicating this message has now be fully downloaded result.setFlag(Flag.X_DOWNLOADED_FULL, true); @@ -1287,14 +1299,14 @@ public class LocalFolder extends Folder implements Serializable { // draft messages because this will cause the values stored in // the identity header to be wrong. ViewableContainer container = - MimeUtility.extractPartsFromDraft(message); + LocalMessageExtractor.extractPartsFromDraft(message); text = container.text; html = container.html; attachments = container.attachments; } else { ViewableContainer container = - MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); + LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); attachments = container.attachments; text = container.text; @@ -1400,7 +1412,7 @@ public class LocalFolder extends Folder implements Serializable { message.buildMimeRepresentation(); ViewableContainer container = - MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); + LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); List attachments = container.attachments; String text = container.text; @@ -1439,12 +1451,12 @@ public class LocalFolder extends Folder implements Serializable { message.isSet(Flag.FLAGGED) ? 1 : 0, message.isSet(Flag.ANSWERED) ? 1 : 0, message.isSet(Flag.FORWARDED) ? 1 : 0, - message.mId + message.getId() }); for (int i = 0, count = attachments.size(); i < count; i++) { Part attachment = attachments.get(i); - saveAttachment(message.mId, attachment, false); + saveAttachment(message.getId(), attachment, false); } saveHeaders(message.getId(), message); } catch (Exception e) { @@ -1473,7 +1485,7 @@ public class LocalFolder extends Folder implements Serializable { 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 { + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException { deleteHeaders(id); for (String name : message.getHeaderNames()) { @@ -1502,7 +1514,7 @@ public class LocalFolder extends Folder implements Serializable { }); } - void deleteHeaders(final long id) throws UnavailableStorageException { + void deleteHeaders(final long id) throws MessagingException { this.localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { @@ -1637,7 +1649,7 @@ public class LocalFolder extends Folder implements Serializable { File attachmentFile = new File(attachmentDirectory, Long.toString(attachmentId)); tempAttachmentFile.renameTo(attachmentFile); contentUri = AttachmentProvider.getAttachmentUri( - mAccount, + getAccount(), attachmentId); if (MimeUtil.isMessage(attachment.getMimeType())) { attachment.setBody(new LocalAttachmentMessageBody( @@ -1712,7 +1724,7 @@ public class LocalFolder extends Folder implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { db.update("messages", cv, "id = ?", new String[] - { Long.toString(message.mId) }); + { Long.toString(message.getId()) }); return null; } }); @@ -1829,7 +1841,7 @@ public class LocalFolder extends Folder implements Serializable { setPushState(null); setLastPush(0); setLastChecked(0); - setVisibleLimit(mAccount.getDisplayCount()); + setVisibleLimit(getAccount().getDisplayCount()); } @Override @@ -1878,7 +1890,7 @@ public class LocalFolder extends Folder implements Serializable { public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { Cursor attachmentsCursor = null; try { - String accountUuid = mAccount.getUuid(); + String accountUuid = getAccountUuid(); Context context = LocalFolder.this.localStore.mApplication; // Get attachment IDs @@ -2039,7 +2051,7 @@ public class LocalFolder extends Folder implements Serializable { // Append the first message ID from the "In-Reply-To" header line String[] inReplyToArray = message.getHeader("In-Reply-To"); - String inReplyTo = null; + String inReplyTo; if (inReplyToArray != null && inReplyToArray.length > 0) { inReplyTo = Utility.extractMessageId(inReplyToArray[0]); if (inReplyTo != null) { @@ -2194,4 +2206,8 @@ public class LocalFolder extends Folder implements Serializable { throw(MessagingException) e.getCause(); } } + + private Account getAccount() { + return localStore.getAccount(); + } } diff --git a/src/com/fsck/k9/mail/store/local/LocalMessage.java b/src/com/fsck/k9/mailstore/LocalMessage.java similarity index 86% rename from src/com/fsck/k9/mail/store/local/LocalMessage.java rename to src/com/fsck/k9/mailstore/LocalMessage.java index 56e20ea8d..ca28ce812 100644 --- a/src/com/fsck/k9/mail/store/local/LocalMessage.java +++ b/src/com/fsck/k9/mailstore/LocalMessage.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store.local; +package com.fsck.k9.mailstore; import java.io.IOException; import java.io.OutputStream; @@ -12,23 +12,25 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.util.Log; +import com.fsck.k9.Account; import com.fsck.k9.K9; +import com.fsck.k9.activity.MessageReference; 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.MessageExtractor; 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.local.LockableDatabase.DbCallback; -import com.fsck.k9.mail.store.local.LockableDatabase.WrappedException; +import com.fsck.k9.mailstore.LockableDatabase.DbCallback; +import com.fsck.k9.mailstore.LockableDatabase.WrappedException; public class LocalMessage extends MimeMessage { - + protected MessageReference mReference; private final LocalStore localStore; - long mId; + private long mId; private int mAttachmentCount; private String mSubject; @@ -50,8 +52,7 @@ public class LocalMessage extends MimeMessage { this.mFolder = folder; } - void populateFromGetMessageCursor(Cursor cursor) - throws MessagingException { + void populateFromGetMessageCursor(Cursor cursor) throws MessagingException { final String subject = cursor.getString(0); this.setSubject(subject == null ? "" : subject); @@ -129,7 +130,7 @@ public class LocalMessage extends MimeMessage { } } else { // We successfully found an HTML part; do the necessary character set decoding. - text = MimeUtility.getTextFromPart(part); + text = MessageExtractor.getTextFromPart(this); } return text; } @@ -156,7 +157,7 @@ public class LocalMessage extends MimeMessage { } super.setReplyTo(mReplyTo); - super.setSentDate(this.getSentDate()); + super.setSentDate(this.getSentDate(), K9.hideTimeZone()); super.setRecipients(RecipientType.TO, mTo); super.setRecipients(RecipientType.CC, mCc); super.setRecipients(RecipientType.BCC, mBcc); @@ -189,6 +190,12 @@ public class LocalMessage extends MimeMessage { mMessageDirty = true; } + @Override + public void setUid(String uid) { + super.setUid(uid); + this.mReference = null; + } + @Override public boolean hasAttachments() { return (mAttachmentCount > 0); @@ -492,44 +499,44 @@ public class LocalMessage extends MimeMessage { db.delete("threads", "message_id = ?", idArg); } - private void loadHeaders() throws UnavailableStorageException { + private void loadHeaders() throws MessagingException { List messages = new ArrayList(); messages.add(this); mHeadersLoaded = true; // set true before calling populate headers to stop recursion - ((LocalFolder) mFolder).populateHeaders(messages); + getFolder().populateHeaders(messages); } @Override - public void addHeader(String name, String value) throws UnavailableStorageException { + public void addHeader(String name, String value) throws MessagingException { if (!mHeadersLoaded) loadHeaders(); super.addHeader(name, value); } @Override - public void setHeader(String name, String value) throws UnavailableStorageException { + public void setHeader(String name, String value) throws MessagingException { if (!mHeadersLoaded) loadHeaders(); super.setHeader(name, value); } @Override - public String[] getHeader(String name) throws UnavailableStorageException { + public String[] getHeader(String name) throws MessagingException { if (!mHeadersLoaded) loadHeaders(); return super.getHeader(name); } @Override - public void removeHeader(String name) throws UnavailableStorageException { + public void removeHeader(String name) throws MessagingException { if (!mHeadersLoaded) loadHeaders(); super.removeHeader(name); } @Override - public Set getHeaderNames() throws UnavailableStorageException { + public Set getHeaderNames() throws MessagingException { if (!mHeadersLoaded) loadHeaders(); return super.getHeaderNames(); @@ -557,4 +564,56 @@ public class LocalMessage extends MimeMessage { public long getRootId() { return mRootId; } -} \ No newline at end of file + + public Account getAccount() { + return localStore.getAccount(); + } + + public MessageReference makeMessageReference() { + if (mReference == null) { + mReference = new MessageReference(); + mReference.folderName = getFolder().getName(); + mReference.uid = mUid; + mReference.accountUuid = getFolder().getAccountUuid(); + } + return mReference; + } + + @Override + protected void copy(MimeMessage destination) { + super.copy(destination); + if (destination instanceof LocalMessage) { + ((LocalMessage)destination).mReference = mReference; + } + } + + @Override + public LocalFolder getFolder() { + return (LocalFolder) super.getFolder(); + } + + public String getUri() { + return "email://messages/" + getAccount().getAccountNumber() + "/" + getFolder().getName() + "/" + getUid(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + final LocalMessage that = (LocalMessage) o; + return !(getAccountUuid() != null ? !getAccountUuid().equals(that.getAccountUuid()) : that.getAccountUuid() != null); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (getAccountUuid() != null ? getAccountUuid().hashCode() : 0); + return result; + } + + private String getAccountUuid() { + return getAccount().getUuid(); + } +} diff --git a/src/com/fsck/k9/mailstore/LocalMessageExtractor.java b/src/com/fsck/k9/mailstore/LocalMessageExtractor.java new file mode 100644 index 000000000..864b9132f --- /dev/null +++ b/src/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -0,0 +1,470 @@ +package com.fsck.k9.mailstore; + +import android.content.Context; + +import com.fsck.k9.R; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.Viewable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; +import static com.fsck.k9.mail.internet.Viewable.Alternative; +import static com.fsck.k9.mail.internet.Viewable.Html; +import static com.fsck.k9.mail.internet.Viewable.MessageHeader; +import static com.fsck.k9.mail.internet.Viewable.Text; +import static com.fsck.k9.mail.internet.Viewable.Textual; + +class LocalMessageExtractor { + private static final String TEXT_DIVIDER = + "------------------------------------------------------------------------"; + private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); + private static final String FILENAME_PREFIX = "----- "; + private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); + private static final String FILENAME_SUFFIX = " "; + private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); + + private LocalMessageExtractor() {} + /** + * Extract the viewable textual parts of a message and return the rest as attachments. + * + * @param context A {@link android.content.Context} instance that will be used to get localized strings. + * @return A {@link ViewableContainer} instance containing the textual parts of the message as + * plain text and HTML, and a list of message parts considered attachments. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + public static ViewableContainer extractTextAndAttachments(Context context, Message message) throws MessagingException { + try { + List attachments = new ArrayList(); + + // Collect all viewable parts + List viewables = MessageExtractor.getViewables(message, attachments); + + /* + * Convert the tree of viewable parts into text and HTML + */ + + // Used to suppress the divider for the first viewable part + boolean hideDivider = true; + + StringBuilder text = new StringBuilder(); + StringBuilder html = new StringBuilder(); + + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + // This is either a text/plain or text/html part. Fill the variables 'text' and + // 'html', converting between plain text and HTML as necessary. + text.append(buildText(viewable, !hideDivider)); + html.append(buildHtml(viewable, !hideDivider)); + hideDivider = false; + } else if (viewable instanceof MessageHeader) { + MessageHeader header = (MessageHeader) viewable; + Part containerPart = header.getContainerPart(); + Message innerMessage = header.getMessage(); + + addTextDivider(text, containerPart, !hideDivider); + addMessageHeaderText(context, text, innerMessage); + + addHtmlDivider(html, containerPart, !hideDivider); + addMessageHeaderHtml(context, html, innerMessage); + + hideDivider = true; + } else if (viewable instanceof Alternative) { + // Handle multipart/alternative contents + Alternative alternative = (Alternative) viewable; + + /* + * We made sure at least one of text/plain or text/html is present when + * creating the Alternative object. If one part is not present we convert the + * other one to make sure 'text' and 'html' always contain the same text. + */ + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + // Fill the 'text' variable + boolean divider = !hideDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + + // Fill the 'html' variable + divider = !hideDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + hideDivider = false; + } + } + + return new ViewableContainer(text.toString(), html.toString(), attachments); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } + + public static ViewableContainer extractPartsFromDraft(Message message) + throws MessagingException { + + Body body = message.getBody(); + if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) { + MimeMultipart multipart = (MimeMultipart) body; + + ViewableContainer container; + int count = multipart.getCount(); + if (count >= 1) { + // The first part is either a text/plain or a multipart/alternative + BodyPart firstPart = multipart.getBodyPart(0); + container = extractTextual(firstPart); + + // The rest should be attachments + for (int i = 1; i < count; i++) { + BodyPart bodyPart = multipart.getBodyPart(i); + container.attachments.add(bodyPart); + } + } else { + container = new ViewableContainer("", "", new ArrayList()); + } + + return container; + } + + return extractTextual(message); + } + + /** + * Use the contents of a {@link com.fsck.k9.mail.internet.Viewable} to create the HTML to be displayed. + * + *

    + * This will use {@link com.fsck.k9.helper.HtmlConverter#textToHtml(String)} to convert plain text parts + * to HTML if necessary. + *

    + * + * @param viewable + * The viewable part to build the HTML from. + * @param prependDivider + * {@code true}, if the HTML divider should be inserted as first element. + * {@code false}, otherwise. + * + * @return The contents of the supplied viewable instance as HTML. + */ + private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider) + { + StringBuilder html = new StringBuilder(); + if (viewable instanceof Textual) { + Part part = ((Textual)viewable).getPart(); + addHtmlDivider(html, part, prependDivider); + + String t = MessageExtractor.getTextFromPart(part); + if (t == null) { + t = ""; + } else if (viewable instanceof Text) { + t = HtmlConverter.textToHtml(t); + } + html.append(t); + } else if (viewable instanceof Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/html child; fall-back to the text/plain part. + Alternative alternative = (Alternative) viewable; + + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + boolean divider = prependDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + } + + return html; + } + + private static StringBuilder buildText(Viewable viewable, boolean prependDivider) + { + StringBuilder text = new StringBuilder(); + if (viewable instanceof Textual) { + Part part = ((Textual)viewable).getPart(); + addTextDivider(text, part, prependDivider); + + String t = MessageExtractor.getTextFromPart(part); + if (t == null) { + t = ""; + } else if (viewable instanceof Html) { + t = HtmlConverter.htmlToText(t); + } + text.append(t); + } else if (viewable instanceof Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/plain child; fall-back to the text/html part. + Alternative alternative = (Alternative) viewable; + + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + + boolean divider = prependDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + } + + return text; + } + + /** + * Add an HTML divider between two HTML message parts. + * + * @param html + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + html.append("

    "); + html.append(filename); + html.append("

    "); + } + } + + /** + * Get the name of the message part. + * + * @param part + * The part to get the name for. + * + * @return The (file)name of the part if available. An empty string, otherwise. + */ + private static String getPartName(Part part) { + try { + String disposition = part.getDisposition(); + if (disposition != null) { + String name = getHeaderParameter(disposition, "filename"); + return (name == null) ? "" : name; + } + } + catch (MessagingException e) { /* ignore */ } + + return ""; + } + + /** + * Add a plain text divider between two plain text message parts. + * + * @param text + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + text.append("\r\n\r\n"); + int len = filename.length(); + if (len > 0) { + if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) { + filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - + FILENAME_SUFFIX_LENGTH - 3) + "..."; + } + text.append(FILENAME_PREFIX); + text.append(filename); + text.append(FILENAME_SUFFIX); + text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - + FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); + } else { + text.append(TEXT_DIVIDER); + } + text.append("\r\n\r\n"); + } + } + + /** + * Extract important header values from a message to display inline (plain text version). + * + * @param context + * A {@link android.content.Context} instance that will be used to get localized strings. + * @param text + * The {@link StringBuilder} that will receive the (plain text) output. + * @param message + * The message to extract the header values from. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + private static void addMessageHeaderText(Context context, StringBuilder text, Message message) + throws MessagingException { + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_from)); + text.append(' '); + text.append(Address.toString(from)); + text.append("\r\n"); + } + + // To: + Address[] to = message.getRecipients(Message.RecipientType.TO); + if (to != null && to.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_to)); + text.append(' '); + text.append(Address.toString(to)); + text.append("\r\n"); + } + + // Cc: + Address[] cc = message.getRecipients(Message.RecipientType.CC); + if (cc != null && cc.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_cc)); + text.append(' '); + text.append(Address.toString(cc)); + text.append("\r\n"); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + text.append(context.getString(R.string.message_compose_quote_header_send_date)); + text.append(' '); + text.append(date.toString()); + text.append("\r\n"); + } + + // Subject: + String subject = message.getSubject(); + text.append(context.getString(R.string.message_compose_quote_header_subject)); + text.append(' '); + if (subject == null) { + text.append(context.getString(R.string.general_no_subject)); + } else { + text.append(subject); + } + text.append("\r\n\r\n"); + } + + /** + * Extract important header values from a message to display inline (HTML version). + * + * @param context + * A {@link android.content.Context} instance that will be used to get localized strings. + * @param html + * The {@link StringBuilder} that will receive the (HTML) output. + * @param message + * The message to extract the header values from. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message) + throws MessagingException { + + html.append(""); + + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_from), + Address.toString(from)); + } + + // To: + Address[] to = message.getRecipients(Message.RecipientType.TO); + if (to != null && to.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_to), + Address.toString(to)); + } + + // Cc: + Address[] cc = message.getRecipients(Message.RecipientType.CC); + if (cc != null && cc.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_cc), + Address.toString(cc)); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date), + date.toString()); + } + + // Subject: + String subject = message.getSubject(); + addTableRow(html, context.getString(R.string.message_compose_quote_header_subject), + (subject == null) ? context.getString(R.string.general_no_subject) : subject); + + html.append("
    "); + } + + /** + * Output an HTML table two column row with some hardcoded style. + * + * @param html + * The {@link StringBuilder} that will receive the output. + * @param header + * The string to be put in the {@code TH} element. + * @param value + * The string to be put in the {@code TD} element. + */ + private static void addTableRow(StringBuilder html, String header, String value) { + html.append(""); + html.append(header); + html.append(""); + html.append(""); + html.append(value); + html.append(""); + } + + private static ViewableContainer extractTextual(Part part) throws MessagingException { + String text = ""; + String html = ""; + List attachments = new ArrayList(); + + Body firstBody = part.getBody(); + if (part.isMimeType("text/plain")) { + String bodyText = MessageExtractor.getTextFromPart(part); + if (bodyText != null) { + text = bodyText; + html = HtmlConverter.textToHtml(text); + } + } else if (part.isMimeType("multipart/alternative") && + firstBody instanceof MimeMultipart) { + MimeMultipart multipart = (MimeMultipart) firstBody; + for (BodyPart bodyPart : multipart.getBodyParts()) { + String bodyText = MessageExtractor.getTextFromPart(bodyPart); + if (bodyText != null) { + if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { + text = bodyText; + } else if (html.isEmpty() && bodyPart.isMimeType("text/html")) { + html = bodyText; + } + } + } + } + return new ViewableContainer(text, html, attachments); + } +} diff --git a/src/com/fsck/k9/mail/store/local/LocalStore.java b/src/com/fsck/k9/mailstore/LocalStore.java similarity index 92% rename from src/com/fsck/k9/mail/store/local/LocalStore.java rename to src/com/fsck/k9/mailstore/LocalStore.java index 02002c125..46dfaa0b3 100644 --- a/src/com/fsck/k9/mail/store/local/LocalStore.java +++ b/src/com/fsck/k9/mailstore/LocalStore.java @@ -1,15 +1,5 @@ -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; +package com.fsck.k9.mailstore; import android.app.Application; import android.content.ContentResolver; @@ -21,22 +11,20 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.text.TextUtils; 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.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; -import com.fsck.k9.mail.store.local.LockableDatabase.DbCallback; -import com.fsck.k9.mail.store.local.LockableDatabase.WrappedException; -import com.fsck.k9.mail.store.StorageManager; -import com.fsck.k9.mail.store.StorageManager.StorageProvider; -import com.fsck.k9.mail.store.UnavailableStorageException; +import com.fsck.k9.mailstore.StorageManager.StorageProvider; +import com.fsck.k9.mail.store.StoreConfig; +import com.fsck.k9.mailstore.LockableDatabase.DbCallback; +import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.search.LocalSearch; @@ -44,6 +32,18 @@ import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; import com.fsck.k9.search.SqlQueryBuilder; +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 java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + /** *
      * Implements a SQLite database backed local store for Messages.
    @@ -56,6 +56,18 @@ public class LocalStore extends Store implements Serializable {
         static final String[] EMPTY_STRING_ARRAY = new String[0];
         static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
     
    +    /**
    +     * Lock objects indexed by account UUID.
    +     *
    +     * @see #getInstance(Account, Application)
    +     */
    +    private static ConcurrentMap sAccountLocks = new ConcurrentHashMap();
    +
    +    /**
    +     * Local stores indexed by UUID because the Uri may change due to migration to/from SD-card.
    +     */
    +    private static ConcurrentMap sLocalStores = new ConcurrentHashMap();
    +
         /*
          * a String containing the columns getMessages expects to work with
          * in the correct order.
    @@ -138,6 +150,7 @@ public class LocalStore extends Store implements Serializable {
         LockableDatabase database;
     
         private ContentResolver mContentResolver;
    +    private final Account mAccount;
     
         /**
          * local://localhost/path/to/database/uuid.db
    @@ -147,7 +160,7 @@ public class LocalStore extends Store implements Serializable {
          * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)}
          */
         public LocalStore(final Account account, final Application application) throws MessagingException {
    -        super(account);
    +        mAccount = account;
             database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition(this));
     
             mApplication = application;
    @@ -158,15 +171,66 @@ public class LocalStore extends Store implements Serializable {
             database.open();
         }
     
    +    /**
    +     * Get an instance of a local mail store.
    +     *
    +     * @throws UnavailableStorageException
    +     *          if not {@link StorageProvider#isReady(Context)}
    +     */
    +    public static LocalStore getInstance(Account account, Application application)
    +            throws MessagingException {
    +
    +        String accountUuid = account.getUuid();
    +
    +        // Create new per-account lock object if necessary
    +        sAccountLocks.putIfAbsent(accountUuid, new Object());
    +
    +        // Get the account's lock object
    +        Object lock = sAccountLocks.get(accountUuid);
    +
    +        // Use per-account locks so DatabaseUpgradeService always knows which account database is
    +        // currently upgraded.
    +        synchronized (lock) {
    +            LocalStore store = sLocalStores.get(accountUuid);
    +
    +            if (store == null) {
    +                // Creating a LocalStore instance will create or upgrade the database if
    +                // necessary. This could take some time.
    +                store = new LocalStore(account, application);
    +
    +                sLocalStores.put(accountUuid, store);
    +            }
    +
    +            return store;
    +        }
    +    }
    +
    +    public static void removeAccount(Account account) {
    +        try {
    +            removeInstance(account);
    +        } catch (Exception e) {
    +            Log.e(K9.LOG_TAG, "Failed to reset local store for account " + account.getUuid(), e);
    +        }
    +    }
    +
    +    private static void removeInstance(Account account) {
    +        String accountUuid = account.getUuid();
    +        sLocalStores.remove(accountUuid);
    +    }
    +
         public void switchLocalStorage(final String newStorageProviderId) throws MessagingException {
             database.switchProvider(newStorageProviderId);
         }
     
    +    protected Account getAccount() {
    +        return mAccount;
    +    }
    +
         protected SharedPreferences getPreferences() {
             return Preferences.getPreferences(mApplication).getPreferences();
         }
     
    -    public long getSize() throws UnavailableStorageException {
    +    public long getSize() throws MessagingException {
     
             final StorageManager storageManager = StorageManager.getInstance(mApplication);
     
    @@ -409,7 +473,7 @@ public class LocalStore extends Store implements Serializable {
             });
         }
     
    -    public void resetVisibleLimits(int visibleLimit) throws UnavailableStorageException {
    +    public void resetVisibleLimits(int visibleLimit) throws MessagingException {
             final ContentValues cv = new ContentValues();
             cv.put("visible_limit", Integer.toString(visibleLimit));
             database.execute(false, new DbCallback() {
    @@ -421,7 +485,7 @@ public class LocalStore extends Store implements Serializable {
             });
         }
     
    -    public List getPendingCommands() throws UnavailableStorageException {
    +    public List getPendingCommands() throws MessagingException {
             return database.execute(false, new DbCallback>() {
                 @Override
                 public List doDbWork(final SQLiteDatabase db) throws WrappedException {
    @@ -454,7 +518,7 @@ public class LocalStore extends Store implements Serializable {
             });
         }
     
    -    public void addPendingCommand(PendingCommand command) throws UnavailableStorageException {
    +    public void addPendingCommand(PendingCommand command) throws MessagingException {
             for (int i = 0; i < command.arguments.length; i++) {
                 command.arguments[i] = UrlEncodingHelper.encodeUtf8(command.arguments[i]);
             }
    @@ -470,7 +534,7 @@ public class LocalStore extends Store implements Serializable {
             });
         }
     
    -    public void removePendingCommand(final PendingCommand command) throws UnavailableStorageException {
    +    public void removePendingCommand(final PendingCommand command) throws MessagingException {
             database.execute(false, new DbCallback() {
                 @Override
                 public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
    @@ -480,7 +544,7 @@ public class LocalStore extends Store implements Serializable {
             });
         }
     
    -    public void removePendingCommands() throws UnavailableStorageException {
    +    public void removePendingCommands() throws MessagingException {
             database.execute(false, new DbCallback() {
                 @Override
                 public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
    @@ -612,7 +676,7 @@ public class LocalStore extends Store implements Serializable {
             return searchForMessages(null, search);
         }
     
    -    public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException {
    +    public AttachmentInfo getAttachmentInfo(final String attachmentId) throws MessagingException {
             return database.execute(false, new DbCallback() {
                 @Override
                 public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException {
    @@ -653,7 +717,7 @@ public class LocalStore extends Store implements Serializable {
             public String type;
         }
     
    -    public void createFolders(final List foldersToCreate, final int visibleLimit) throws UnavailableStorageException {
    +    public void createFolders(final List foldersToCreate, final int visibleLimit) throws MessagingException {
             database.execute(true, new DbCallback() {
                 @Override
                 public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
    diff --git a/src/com/fsck/k9/mail/store/local/LocalTextBody.java b/src/com/fsck/k9/mailstore/LocalTextBody.java
    similarity index 92%
    rename from src/com/fsck/k9/mail/store/local/LocalTextBody.java
    rename to src/com/fsck/k9/mailstore/LocalTextBody.java
    index 3d83c6eb9..57c4963cf 100644
    --- a/src/com/fsck/k9/mail/store/local/LocalTextBody.java
    +++ b/src/com/fsck/k9/mailstore/LocalTextBody.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     import com.fsck.k9.mail.internet.TextBody;
     
    diff --git a/src/com/fsck/k9/mail/store/local/LockableDatabase.java b/src/com/fsck/k9/mailstore/LockableDatabase.java
    similarity index 97%
    rename from src/com/fsck/k9/mail/store/local/LockableDatabase.java
    rename to src/com/fsck/k9/mailstore/LockableDatabase.java
    index 57cae16ef..f7c0a2258 100644
    --- a/src/com/fsck/k9/mail/store/local/LockableDatabase.java
    +++ b/src/com/fsck/k9/mailstore/LockableDatabase.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     import java.io.File;
     import java.util.concurrent.locks.Lock;
    @@ -16,8 +16,6 @@ import android.util.Log;
     import com.fsck.k9.K9;
     import com.fsck.k9.helper.FileHelper;
     import com.fsck.k9.mail.MessagingException;
    -import com.fsck.k9.mail.store.StorageManager;
    -import com.fsck.k9.mail.store.UnavailableStorageException;
     
     public class LockableDatabase {
     
    @@ -35,9 +33,10 @@ public class LockableDatabase {
              *            null.
              * @return Any relevant data. Can be null.
              * @throws WrappedException
    -         * @throws com.fsck.k9.mail.store.UnavailableStorageException
    +         * @throws com.fsck.k9.mail.MessagingException
    +         * @throws com.fsck.k9.mailstore.UnavailableStorageException
              */
    -        T doDbWork(SQLiteDatabase db) throws WrappedException, UnavailableStorageException;
    +        T doDbWork(SQLiteDatabase db) throws WrappedException, MessagingException;
         }
     
         public static interface SchemaDefinition {
    @@ -272,7 +271,7 @@ public class LockableDatabase {
          * @return Whatever {@link DbCallback#doDbWork(SQLiteDatabase)} returns.
          * @throws UnavailableStorageException
          */
    -    public  T execute(final boolean transactional, final DbCallback callback) throws UnavailableStorageException {
    +    public  T execute(final boolean transactional, final DbCallback callback) throws MessagingException {
             lockRead();
             final boolean doTransaction = transactional && inTransaction.get() == null;
             try {
    diff --git a/src/com/fsck/k9/controller/MessageRemovalListener.java b/src/com/fsck/k9/mailstore/MessageRemovalListener.java
    similarity index 80%
    rename from src/com/fsck/k9/controller/MessageRemovalListener.java
    rename to src/com/fsck/k9/mailstore/MessageRemovalListener.java
    index 38973dde4..51ad9a15e 100644
    --- a/src/com/fsck/k9/controller/MessageRemovalListener.java
    +++ b/src/com/fsck/k9/mailstore/MessageRemovalListener.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.controller;
    +package com.fsck.k9.mailstore;
     
     import com.fsck.k9.mail.Message;
     
    diff --git a/src/com/fsck/k9/mail/store/StorageManager.java b/src/com/fsck/k9/mailstore/StorageManager.java
    similarity index 99%
    rename from src/com/fsck/k9/mail/store/StorageManager.java
    rename to src/com/fsck/k9/mailstore/StorageManager.java
    index 68cc216df..3aaf663dc 100644
    --- a/src/com/fsck/k9/mail/store/StorageManager.java
    +++ b/src/com/fsck/k9/mailstore/StorageManager.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store;
    +package com.fsck.k9.mailstore;
     
     import java.io.File;
     import java.io.IOException;
    diff --git a/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java b/src/com/fsck/k9/mailstore/StoreSchemaDefinition.java
    similarity index 99%
    rename from src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java
    rename to src/com/fsck/k9/mailstore/StoreSchemaDefinition.java
    index aec7390fc..f9e786dba 100644
    --- a/src/com/fsck/k9/mail/store/local/StoreSchemaDefinition.java
    +++ b/src/com/fsck/k9/mailstore/StoreSchemaDefinition.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     import java.util.ArrayList;
     import java.util.List;
    diff --git a/src/com/fsck/k9/mail/store/local/TempFileBody.java b/src/com/fsck/k9/mailstore/TempFileBody.java
    similarity index 94%
    rename from src/com/fsck/k9/mail/store/local/TempFileBody.java
    rename to src/com/fsck/k9/mailstore/TempFileBody.java
    index 9b7916b5a..934b3bd68 100644
    --- a/src/com/fsck/k9/mail/store/local/TempFileBody.java
    +++ b/src/com/fsck/k9/mailstore/TempFileBody.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     import java.io.ByteArrayInputStream;
     import java.io.File;
    diff --git a/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java b/src/com/fsck/k9/mailstore/TempFileMessageBody.java
    similarity index 96%
    rename from src/com/fsck/k9/mail/store/local/TempFileMessageBody.java
    rename to src/com/fsck/k9/mailstore/TempFileMessageBody.java
    index 5cd5e207b..c310ae2e4 100644
    --- a/src/com/fsck/k9/mail/store/local/TempFileMessageBody.java
    +++ b/src/com/fsck/k9/mailstore/TempFileMessageBody.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     import java.io.IOException;
     import java.io.OutputStream;
    diff --git a/src/com/fsck/k9/mail/store/local/ThreadInfo.java b/src/com/fsck/k9/mailstore/ThreadInfo.java
    similarity index 92%
    rename from src/com/fsck/k9/mail/store/local/ThreadInfo.java
    rename to src/com/fsck/k9/mailstore/ThreadInfo.java
    index 4115c16d6..9f9ee2204 100644
    --- a/src/com/fsck/k9/mail/store/local/ThreadInfo.java
    +++ b/src/com/fsck/k9/mailstore/ThreadInfo.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store.local;
    +package com.fsck.k9.mailstore;
     
     class ThreadInfo {
         public final long threadId;
    diff --git a/src/com/fsck/k9/mail/store/UnavailableStorageException.java b/src/com/fsck/k9/mailstore/UnavailableStorageException.java
    similarity index 96%
    rename from src/com/fsck/k9/mail/store/UnavailableStorageException.java
    rename to src/com/fsck/k9/mailstore/UnavailableStorageException.java
    index 90974161b..d764a0e39 100644
    --- a/src/com/fsck/k9/mail/store/UnavailableStorageException.java
    +++ b/src/com/fsck/k9/mailstore/UnavailableStorageException.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.store;
    +package com.fsck.k9.mailstore;
     
     import com.fsck.k9.mail.MessagingException;
     
    diff --git a/src/com/fsck/k9/mailstore/ViewableContainer.java b/src/com/fsck/k9/mailstore/ViewableContainer.java
    new file mode 100644
    index 000000000..2e538cd22
    --- /dev/null
    +++ b/src/com/fsck/k9/mailstore/ViewableContainer.java
    @@ -0,0 +1,34 @@
    +package com.fsck.k9.mailstore;
    +
    +import com.fsck.k9.mail.Part;
    +
    +import java.util.List;
    +
    +/**
    + * Store viewable text of a message as plain text and HTML, and the parts considered
    + * attachments.
    + *
    + * @see LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message)
    + */
    +class ViewableContainer {
    +    /**
    +     * The viewable text of the message in plain text.
    +     */
    +    public final String text;
    +
    +    /**
    +     * The viewable text of the message in HTML.
    +     */
    +    public final String html;
    +
    +    /**
    +     * The parts of the message considered attachments (everything not viewable).
    +     */
    +    public final List attachments;
    +
    +    public ViewableContainer(String text, String html, List attachments) {
    +        this.text = text;
    +        this.html = html;
    +        this.attachments = attachments;
    +    }
    +}
    diff --git a/src/com/fsck/k9/preferences/AccountSettings.java b/src/com/fsck/k9/preferences/AccountSettings.java
    index c2b6b4983..6bdcb4699 100644
    --- a/src/com/fsck/k9/preferences/AccountSettings.java
    +++ b/src/com/fsck/k9/preferences/AccountSettings.java
    @@ -13,7 +13,7 @@ import com.fsck.k9.Account.SortType;
     import com.fsck.k9.K9;
     import com.fsck.k9.R;
     import com.fsck.k9.Account.FolderMode;
    -import com.fsck.k9.mail.store.StorageManager;
    +import com.fsck.k9.mailstore.StorageManager;
     import com.fsck.k9.preferences.Settings.*;
     
     public class AccountSettings {
    diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java
    index 6085f3014..f49ec7a20 100644
    --- a/src/com/fsck/k9/preferences/SettingsExporter.java
    +++ b/src/com/fsck/k9/preferences/SettingsExporter.java
    @@ -25,9 +25,9 @@ import android.util.Xml;
     import com.fsck.k9.Account;
     import com.fsck.k9.K9;
     import com.fsck.k9.Preferences;
    -import com.fsck.k9.mail.Store;
     import com.fsck.k9.mail.ServerSettings;
     import com.fsck.k9.mail.Transport;
    +import com.fsck.k9.mail.store.RemoteStore;
     import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
     import com.fsck.k9.preferences.Settings.SettingsDescription;
     
    @@ -223,7 +223,7 @@ public class SettingsExporter {
     
     
             // Write incoming server settings
    -        ServerSettings incoming = Store.decodeStoreUri(account.getStoreUri());
    +        ServerSettings incoming = RemoteStore.decodeStoreUri(account.getStoreUri());
             serializer.startTag(null, INCOMING_SERVER_ELEMENT);
             serializer.attribute(null, TYPE_ATTRIBUTE, incoming.type);
     
    diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java
    index b3143f724..1d05cf9dc 100644
    --- a/src/com/fsck/k9/preferences/SettingsImporter.java
    +++ b/src/com/fsck/k9/preferences/SettingsImporter.java
    @@ -22,12 +22,12 @@ import com.fsck.k9.Account;
     import com.fsck.k9.Identity;
     import com.fsck.k9.K9;
     import com.fsck.k9.Preferences;
    -import com.fsck.k9.helper.Utility;
     import com.fsck.k9.mail.AuthType;
     import com.fsck.k9.mail.ConnectionSecurity;
     import com.fsck.k9.mail.ServerSettings;
    -import com.fsck.k9.mail.Store;
     import com.fsck.k9.mail.Transport;
    +import com.fsck.k9.mail.filter.Base64;
    +import com.fsck.k9.mail.store.RemoteStore;
     import com.fsck.k9.mail.store.WebDavStore;
     import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
     
    @@ -376,8 +376,8 @@ public class SettingsImporter {
     
             // Write incoming server settings (storeUri)
             ServerSettings incoming = new ImportedServerSettings(account.incoming);
    -        String storeUri = Store.createStoreUri(incoming);
    -        putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri));
    +        String storeUri = RemoteStore.createStoreUri(incoming);
    +        putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Base64.encode(storeUri));
     
             // Mark account as disabled if the AuthType isn't EXTERNAL and the
             // settings file didn't contain a password
    @@ -393,7 +393,7 @@ public class SettingsImporter {
                 // Write outgoing server settings (transportUri)
                 ServerSettings outgoing = new ImportedServerSettings(account.outgoing);
                 String transportUri = Transport.createTransportUri(outgoing);
    -            putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri));
    +            putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Base64.encode(transportUri));
     
                 /*
                  * Mark account as disabled if the settings file contained a
    diff --git a/src/com/fsck/k9/preferences/Storage.java b/src/com/fsck/k9/preferences/Storage.java
    index 88260f7ff..1303fc8e9 100644
    --- a/src/com/fsck/k9/preferences/Storage.java
    +++ b/src/com/fsck/k9/preferences/Storage.java
    @@ -11,9 +11,9 @@ import android.util.Log;
     import com.fsck.k9.K9;
     import com.fsck.k9.helper.UrlEncodingHelper;
     import com.fsck.k9.helper.Utility;
    +import com.fsck.k9.mail.filter.Base64;
     
     import java.net.URI;
    -import java.net.URLEncoder;
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Map;
    @@ -54,8 +54,8 @@ public class Storage implements SharedPreferences {
                     String[] uuids = accountUuids.split(",");
                     for (String uuid : uuids) {
                         try {
    -                        String storeUriStr = Utility.base64Decode(readValue(mDb, uuid + ".storeUri"));
    -                        String transportUriStr = Utility.base64Decode(readValue(mDb, uuid + ".transportUri"));
    +                        String storeUriStr = Base64.decode(readValue(mDb, uuid + ".storeUri"));
    +                        String transportUriStr = Base64.decode(readValue(mDb, uuid + ".transportUri"));
     
                             URI uri = new URI(transportUriStr);
                             String newUserInfo = null;
    @@ -77,7 +77,7 @@ public class Storage implements SharedPreferences {
     
                             if (newUserInfo != null) {
                                 URI newUri = new URI(uri.getScheme(), newUserInfo, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
    -                            String newTransportUriStr = Utility.base64Encode(newUri.toString());
    +                            String newTransportUriStr = Base64.encode(newUri.toString());
                                 writeValue(mDb, uuid + ".transportUri", newTransportUriStr);
                             }
     
    @@ -121,7 +121,7 @@ public class Storage implements SharedPreferences {
     
                             if (newUserInfo != null) {
                                 URI newUri = new URI(uri.getScheme(), newUserInfo, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
    -                            String newStoreUriStr = Utility.base64Encode(newUri.toString());
    +                            String newStoreUriStr = Base64.encode(newUri.toString());
                                 writeValue(mDb, uuid + ".storeUri", newStoreUriStr);
                             }
                         } catch (Exception e) {
    diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java
    index 6f5bf32ac..6516f60ef 100644
    --- a/src/com/fsck/k9/provider/AttachmentProvider.java
    +++ b/src/com/fsck/k9/provider/AttachmentProvider.java
    @@ -15,9 +15,9 @@ 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.local.LocalStore;
    -import com.fsck.k9.mail.store.local.LocalStore.AttachmentInfo;
    -import com.fsck.k9.mail.store.StorageManager;
    +import com.fsck.k9.mailstore.LocalStore;
    +import com.fsck.k9.mailstore.LocalStore.AttachmentInfo;
    +import com.fsck.k9.mailstore.StorageManager;
     
     import java.io.*;
     import java.util.List;
    @@ -215,7 +215,7 @@ public class AttachmentProvider extends ContentProvider {
             final AttachmentInfo attachmentInfo;
             try {
                 final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
    -            attachmentInfo = LocalStore.getLocalInstance(account, K9.app).getAttachmentInfo(id);
    +            attachmentInfo = LocalStore.getInstance(account, K9.app).getAttachmentInfo(id);
             } catch (MessagingException e) {
                 Log.e(K9.LOG_TAG, "Unable to retrieve attachment info from local store for ID: " + id, e);
                 return null;
    @@ -269,7 +269,7 @@ public class AttachmentProvider extends ContentProvider {
                 final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
     
                 try {
    -                final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app);
    +                final LocalStore localStore = LocalStore.getInstance(account, K9.app);
     
                     AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id);
                     if (FORMAT_VIEW.equals(format) && mimeType != null) {
    diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
    index 0164ce4ec..d599f4ac7 100644
    --- a/src/com/fsck/k9/provider/EmailProvider.java
    +++ b/src/com/fsck/k9/provider/EmailProvider.java
    @@ -10,11 +10,11 @@ import com.fsck.k9.Preferences;
     import com.fsck.k9.cache.EmailProviderCacheCursor;
     import com.fsck.k9.helper.Utility;
     import com.fsck.k9.mail.MessagingException;
    -import com.fsck.k9.mail.store.local.LockableDatabase;
    -import com.fsck.k9.mail.store.local.LockableDatabase.DbCallback;
    -import com.fsck.k9.mail.store.local.LockableDatabase.WrappedException;
    -import com.fsck.k9.mail.store.UnavailableStorageException;
    -import com.fsck.k9.mail.store.local.LocalStore;
    +import com.fsck.k9.mailstore.LockableDatabase;
    +import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
    +import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
    +import com.fsck.k9.mailstore.UnavailableStorageException;
    +import com.fsck.k9.mailstore.LocalStore;
     import com.fsck.k9.search.SqlQueryBuilder;
     
     import android.content.ContentProvider;
    @@ -357,6 +357,8 @@ public class EmailProvider extends ContentProvider {
                 });
             } catch (UnavailableStorageException e) {
                 throw new RuntimeException("Storage not available", e);
    +        } catch (MessagingException e) {
    +            throw new RuntimeException("messaging exception", e);
             }
         }
     
    @@ -427,6 +429,8 @@ public class EmailProvider extends ContentProvider {
                 });
             } catch (UnavailableStorageException e) {
                 throw new RuntimeException("Storage not available", e);
    +        } catch (MessagingException e) {
    +            throw new RuntimeException("messaging exception", e);
             }
         }
     
    @@ -532,7 +536,10 @@ public class EmailProvider extends ContentProvider {
                 });
             } catch (UnavailableStorageException e) {
                 throw new RuntimeException("Storage not available", e);
    +        } catch (MessagingException e) {
    +            throw new RuntimeException("messaging exception", e);
             }
    +
         }
     
         private Cursor getAccountStats(String accountUuid, String[] columns,
    @@ -594,7 +601,10 @@ public class EmailProvider extends ContentProvider {
                 });
             }  catch (UnavailableStorageException e) {
                 throw new RuntimeException("Storage not available", e);
    +        } catch (MessagingException e) {
    +            throw new RuntimeException("messaging exception", e);
             }
    +
         }
     
         private Account getAccount(String accountUuid) {
    diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
    index 6ce34baf3..4508edf1b 100644
    --- a/src/com/fsck/k9/provider/MessageProvider.java
    +++ b/src/com/fsck/k9/provider/MessageProvider.java
    @@ -32,8 +32,9 @@ import com.fsck.k9.mail.Flag;
     import com.fsck.k9.mail.Folder;
     import com.fsck.k9.mail.Message;
     import com.fsck.k9.mail.MessagingException;
    -import com.fsck.k9.mail.store.local.LocalMessage;
    -import com.fsck.k9.mail.store.local.LocalStore;
    +import com.fsck.k9.mailstore.LocalFolder;
    +import com.fsck.k9.mailstore.LocalMessage;
    +import com.fsck.k9.mailstore.LocalStore;
     import com.fsck.k9.search.SearchAccount;
     
     import java.lang.ref.WeakReference;
    @@ -200,9 +201,9 @@ public class MessageProvider extends ContentProvider {
         public static class DeleteUriExtractor implements FieldExtractor {
             @Override
             public String getField(final MessageInfoHolder source) {
    -            final Message message = source.message;
    +            final LocalMessage message = source.message;
                 return CONTENT_URI + "/delete_message/"
    -                   + message.getFolder().getAccount().getAccountNumber() + "/"
    +                   + message.getAccount().getAccountNumber() + "/"
                        + message.getFolder().getName() + "/" + message.getUid();
             }
         }
    @@ -221,21 +222,21 @@ public class MessageProvider extends ContentProvider {
         public static class AccountExtractor implements FieldExtractor {
             @Override
             public String getField(final MessageInfoHolder source) {
    -            return source.message.getFolder().getAccount().getDescription();
    +            return source.message.getAccount().getDescription();
             }
         }
     
         public static class AccountColorExtractor implements FieldExtractor {
             @Override
             public Integer getField(final MessageInfoHolder source) {
    -            return source.message.getFolder().getAccount().getChipColor();
    +            return source.message.getAccount().getChipColor();
             }
         }
     
         public static class AccountNumberExtractor implements FieldExtractor {
             @Override
             public Integer getField(final MessageInfoHolder source) {
    -            return source.message.getFolder().getAccount().getAccountNumber();
    +            return source.message.getAccount().getAccountNumber();
             }
         }
     
    @@ -910,18 +911,18 @@ public class MessageProvider extends ContentProvider {
     
             @Override
             public void listLocalMessagesAddMessages(final Account account,
    -                final String folderName, final List messages) {
    +                final String folderName, final List messages) {
                 // cache fields into local variables for faster access on JVM without JIT
                 final MessageHelper helper = mMessageHelper;
                 final List holders = mHolders;
     
                 final Context context = getContext();
     
    -            for (final Message message : messages) {
    +            for (final LocalMessage message : messages) {
                     final MessageInfoHolder messageInfoHolder = new MessageInfoHolder();
                     final Folder messageFolder = message.getFolder();
    -                final Account messageAccount = messageFolder.getAccount();
     
    +                final Account messageAccount = messageInfoHolder.message.getAccount();
                     helper.populate(messageInfoHolder, message, new FolderInfoHolder(context,
                                     messageFolder, messageAccount), messageAccount);
     
    @@ -1038,9 +1039,9 @@ public class MessageProvider extends ContentProvider {
             }
     
             // get localstore parameter
    -        Message msg = null;
    +        LocalMessage msg = null;
             try {
    -            Folder lf = LocalStore.getLocalInstance(myAccount, K9.app).getFolder(folderName);
    +            LocalFolder lf = LocalStore.getInstance(myAccount, K9.app).getFolder(folderName);
                 int msgCount = lf.getMessageCount();
                 if (K9.DEBUG) {
                     Log.d(K9.LOG_TAG, "folder msg count = " + msgCount);
    diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
    index 1b6ecf6a2..118467301 100644
    --- a/src/com/fsck/k9/search/SqlQueryBuilder.java
    +++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
    @@ -5,8 +5,8 @@ import java.util.List;
     import com.fsck.k9.Account;
     import com.fsck.k9.mail.MessagingException;
     import com.fsck.k9.mail.Folder;
    -import com.fsck.k9.mail.store.local.LocalFolder;
    -import com.fsck.k9.mail.store.local.LocalStore;
    +import com.fsck.k9.mailstore.LocalFolder;
    +import com.fsck.k9.mailstore.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 ee5b9b26b..d0fe0b5b4 100644
    --- a/src/com/fsck/k9/service/DatabaseUpgradeService.java
    +++ b/src/com/fsck/k9/service/DatabaseUpgradeService.java
    @@ -17,7 +17,7 @@ import com.fsck.k9.Preferences;
     import com.fsck.k9.activity.UpgradeDatabases;
     import com.fsck.k9.helper.power.TracingPowerManager;
     import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock;
    -import com.fsck.k9.mail.store.UnavailableStorageException;
    +import com.fsck.k9.mailstore.UnavailableStorageException;
     
     /**
      * Service used to upgrade the accounts' databases and/or track the progress of the upgrade.
    diff --git a/src/com/fsck/k9/service/NotificationActionService.java b/src/com/fsck/k9/service/NotificationActionService.java
    index 107bd0f4c..50f14bc15 100644
    --- a/src/com/fsck/k9/service/NotificationActionService.java
    +++ b/src/com/fsck/k9/service/NotificationActionService.java
    @@ -10,9 +10,8 @@ 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;
    +import com.fsck.k9.mailstore.LocalMessage;
     
     import android.app.PendingIntent;
     import android.content.Context;
    @@ -89,10 +88,10 @@ public class NotificationActionService extends CoreService {
     
                     List refs =
                             intent.getParcelableArrayListExtra(EXTRA_MESSAGE_LIST);
    -                List messages = new ArrayList();
    +                List messages = new ArrayList();
     
                     for (MessageReference ref : refs) {
    -                    Message m = ref.restoreToLocalMessage(this);
    +                    LocalMessage m = ref.restoreToLocalMessage(this);
                         if (m != null) {
                             messages.add(m);
                         }
    @@ -103,10 +102,10 @@ public class NotificationActionService extends CoreService {
                     if (K9.DEBUG)
                         Log.i(K9.LOG_TAG, "NotificationActionService initiating reply");
     
    -                MessageReference ref = (MessageReference) intent.getParcelableExtra(EXTRA_MESSAGE);
    -                Message message = ref.restoreToLocalMessage(this);
    +                MessageReference ref = intent.getParcelableExtra(EXTRA_MESSAGE);
    +                LocalMessage message = ref.restoreToLocalMessage(this);
                     if (message != null) {
    -                    Intent i = MessageCompose.getActionReplyIntent(this, account, message, false, null);
    +                    Intent i = MessageCompose.getActionReplyIntent(this, message, false, null);
                         i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                         startActivity(i);
                     } else {
    diff --git a/src/com/fsck/k9/service/StorageGoneReceiver.java b/src/com/fsck/k9/service/StorageGoneReceiver.java
    index 43f9e49d7..592656f9d 100644
    --- a/src/com/fsck/k9/service/StorageGoneReceiver.java
    +++ b/src/com/fsck/k9/service/StorageGoneReceiver.java
    @@ -7,7 +7,7 @@ import android.net.Uri;
     import android.util.Log;
     
     import com.fsck.k9.K9;
    -import com.fsck.k9.mail.store.StorageManager;
    +import com.fsck.k9.mailstore.StorageManager;
     
     /**
      * That BroadcastReceiver is only interested in UNMOUNT events.
    diff --git a/src/com/fsck/k9/service/StorageReceiver.java b/src/com/fsck/k9/service/StorageReceiver.java
    index 515750be4..abf36dd13 100644
    --- a/src/com/fsck/k9/service/StorageReceiver.java
    +++ b/src/com/fsck/k9/service/StorageReceiver.java
    @@ -7,7 +7,7 @@ import android.net.Uri;
     import android.util.Log;
     
     import com.fsck.k9.K9;
    -import com.fsck.k9.mail.store.StorageManager;
    +import com.fsck.k9.mailstore.StorageManager;
     
     /**
      * That BroadcastReceiver is only interested in MOUNT events.
    diff --git a/src/com/fsck/k9/view/AttachmentView.java b/src/com/fsck/k9/view/AttachmentView.java
    index ea917f334..7552c23b5 100644
    --- a/src/com/fsck/k9/view/AttachmentView.java
    +++ b/src/com/fsck/k9/view/AttachmentView.java
    @@ -43,7 +43,7 @@ import com.fsck.k9.mail.MessagingException;
     import com.fsck.k9.mail.Part;
     import com.fsck.k9.mail.internet.MimeHeader;
     import com.fsck.k9.mail.internet.MimeUtility;
    -import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart;
    +import com.fsck.k9.mailstore.LocalAttachmentBodyPart;
     import com.fsck.k9.provider.AttachmentProvider;
     import org.apache.commons.io.IOUtils;
     
    diff --git a/src/com/fsck/k9/view/MessageHeader.java b/src/com/fsck/k9/view/MessageHeader.java
    index 99d02ece4..67c5e130d 100644
    --- a/src/com/fsck/k9/view/MessageHeader.java
    +++ b/src/com/fsck/k9/view/MessageHeader.java
    @@ -217,9 +217,9 @@ public class MessageHeader extends LinearLayout implements OnClickListener {
     
         public void populate(final Message message, final Account account) throws MessagingException {
             final Contacts contacts = K9.showContactName() ? mContacts : null;
    -        final CharSequence from = Address.toFriendly(message.getFrom(), contacts);
    -        final CharSequence to = Address.toFriendly(message.getRecipients(Message.RecipientType.TO), contacts);
    -        final CharSequence cc = Address.toFriendly(message.getRecipients(Message.RecipientType.CC), contacts);
    +        final CharSequence from = MessageHelper.toFriendly(message.getFrom(), contacts);
    +        final CharSequence to = MessageHelper.toFriendly(message.getRecipients(Message.RecipientType.TO), contacts);
    +        final CharSequence cc = MessageHelper.toFriendly(message.getRecipients(Message.RecipientType.CC), contacts);
     
             Address[] fromAddrs = message.getFrom();
             Address[] toAddrs = message.getRecipients(Message.RecipientType.TO);
    diff --git a/src/com/fsck/k9/view/MessageOpenPgpView.java b/src/com/fsck/k9/view/MessageOpenPgpView.java
    index 1b224112f..3c0e857da 100644
    --- a/src/com/fsck/k9/view/MessageOpenPgpView.java
    +++ b/src/com/fsck/k9/view/MessageOpenPgpView.java
    @@ -35,8 +35,9 @@ import com.fsck.k9.helper.IdentityHelper;
     import com.fsck.k9.mail.Message;
     import com.fsck.k9.mail.MessagingException;
     import com.fsck.k9.mail.Part;
    -import com.fsck.k9.mail.internet.MimeUtility;
     
    +import com.fsck.k9.mail.internet.MessageExtractor;
    +import com.fsck.k9.mail.internet.MimeUtility;
     import org.openintents.openpgp.OpenPgpError;
     import org.openintents.openpgp.OpenPgpSignatureResult;
     import org.openintents.openpgp.util.OpenPgpApi;
    @@ -215,8 +216,7 @@ public class MessageOpenPgpView extends LinearLayout {
             } else {
                 try {
                     // check for PGP/MIME encryption
    -                Part pgp = MimeUtility
    -                        .findFirstPartByMimeType(message, "application/pgp-encrypted");
    +                Part pgp = MimeUtility.findFirstPartByMimeType(message, "application/pgp-encrypted");
                     if (pgp != null) {
                         Toast.makeText(mContext, R.string.pgp_mime_unsupported, Toast.LENGTH_LONG)
                                 .show();
    @@ -246,7 +246,7 @@ public class MessageOpenPgpView extends LinearLayout {
                             part = MimeUtility.findFirstPartByMimeType(message, "text/html");
                         }
                         if (part != null) {
    -                        mData = MimeUtility.getTextFromPart(part);
    +                        mData = MessageExtractor.getTextFromPart(part);
                         }
     
                         // wait for service to be bound
    diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java
    index 1506bd4dd..25852ee17 100644
    --- a/src/com/fsck/k9/view/SingleMessageView.java
    +++ b/src/com/fsck/k9/view/SingleMessageView.java
    @@ -5,7 +5,6 @@ import java.io.FileOutputStream;
     import java.io.InputStream;
     import java.net.URL;
     import java.net.URLConnection;
    -import java.net.URLDecoder;
     
     import android.app.Activity;
     import android.app.Fragment;
    @@ -57,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.local.LocalAttachmentBodyPart;
    -import com.fsck.k9.mail.store.local.LocalMessage;
    +import com.fsck.k9.mailstore.LocalAttachmentBodyPart;
    +import com.fsck.k9.mailstore.LocalMessage;
     import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns;
     
     import org.apache.commons.io.IOUtils;
    diff --git a/tests-on-jvm/src/android/text/TextUtils.java b/tests-on-jvm/src/android/text/TextUtils.java
    new file mode 100644
    index 000000000..cdc2ddc19
    --- /dev/null
    +++ b/tests-on-jvm/src/android/text/TextUtils.java
    @@ -0,0 +1,7 @@
    +package android.text;
    +
    +public class TextUtils {
    +    public static boolean isEmpty(CharSequence str) {
    +        return (str == null || str.length() == 0);
    +    }
    +}
    diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java b/tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java
    similarity index 98%
    rename from tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java
    rename to tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java
    index 64882691d..1a924ea0c 100644
    --- a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java
    +++ b/tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.internet;
    +package com.fsck.k9.activity;
     
     import static org.hamcrest.CoreMatchers.*;
     import static org.junit.Assert.*;
    @@ -7,7 +7,8 @@ import org.junit.experimental.theories.*;
     import org.junit.runner.RunWith;
     
     import com.fsck.k9.Account.QuoteStyle;
    -import com.fsck.k9.activity.InsertableHtmlContent;
    +import com.fsck.k9.activity.TextBodyBuilder;
    +import com.fsck.k9.mail.internet.TextBody;
     
     class TestingTextBodyBuilder extends TextBodyBuilder {
     
    diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java b/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java
    new file mode 100644
    index 000000000..1b314642c
    --- /dev/null
    +++ b/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java
    @@ -0,0 +1,94 @@
    +package com.fsck.k9.mail.internet;
    +
    +import junit.framework.TestCase;
    +
    +public class CharsetSupportTest extends TestCase {
    +
    +    public void testFixupCharset() throws Exception {
    +        String charsetOnMail;
    +        String expect;
    +
    +        charsetOnMail = "CP932";
    +        expect = "shift_jis";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, new MimeMessage()));
    +
    +//        charsetOnMail = "koi8-u";
    +//        expect = "koi8-r";
    +//        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, new MimeMessage()));
    +
    +        MimeMessage message;
    +
    +        message= new MimeMessage();
    +        message.setHeader("From", "aaa@docomo.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@dwmail.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@pdx.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@willcom.com");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@emnet.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@emobile.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-docomo-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@softbank.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-softbank-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@vodafone.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-softbank-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@disney.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-softbank-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@vertuclub.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-softbank-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@ezweb.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-kddi-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +        message = new MimeMessage();
    +        message.setHeader("From", "aaa@ido.ne.jp");
    +        charsetOnMail = "shift_jis";
    +        expect = "x-kddi-shift_jis-2007";
    +        assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message));
    +
    +    }
    +}
    \ No newline at end of file
    diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java b/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java
    index e723cb562..fe07dddcb 100644
    --- a/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java
    +++ b/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java
    @@ -8,15 +8,6 @@ import com.fsck.k9.mail.MessagingException;
     import junit.framework.TestCase;
     
     public class MimeUtilityTest extends TestCase {
    -
    -    protected void setUp() throws Exception {
    -        super.setUp();
    -    }
    -
    -    protected void tearDown() throws Exception {
    -        super.tearDown();
    -    }
    -
         public void testGetHeaderParameter() {
             String result;
     
    @@ -55,93 +46,4 @@ public class MimeUtilityTest extends TestCase {
             result = MimeUtility.getHeaderParameter("text/HTML ; charset=\"windows-1251\"", null);
             assertEquals("text/HTML", result);
         }
    -
    -    public void testFixupCharset() throws MessagingException {
    -        String charsetOnMail;
    -        String expect;
    -
    -        charsetOnMail = "CP932";
    -        expect = "shift_jis";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, new MimeMessage()));
    -
    -//        charsetOnMail = "koi8-u";
    -//        expect = "koi8-r";
    -//        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, new MimeMessage()));
    -
    -        MimeMessage message;
    -
    -        message= new MimeMessage();
    -        message.setHeader("From", "aaa@docomo.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@dwmail.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@pdx.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@willcom.com");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@emnet.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@emobile.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-docomo-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@softbank.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-softbank-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@vodafone.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-softbank-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@disney.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-softbank-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@vertuclub.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-softbank-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@ezweb.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-kddi-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -        message = new MimeMessage();
    -        message.setHeader("From", "aaa@ido.ne.jp");
    -        charsetOnMail = "shift_jis";
    -        expect = "x-kddi-shift_jis-2007";
    -        assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message));
    -
    -    }
    -
     }
    diff --git a/tests/src/com/fsck/k9/helper/MessageHelperTest.java b/tests/src/com/fsck/k9/helper/MessageHelperTest.java
    new file mode 100644
    index 000000000..0d71784e3
    --- /dev/null
    +++ b/tests/src/com/fsck/k9/helper/MessageHelperTest.java
    @@ -0,0 +1,63 @@
    +package com.fsck.k9.helper;
    +
    +
    +import android.graphics.Color;
    +import android.test.AndroidTestCase;
    +import android.text.SpannableString;
    +
    +import com.fsck.k9.mail.Address;
    +
    +public class MessageHelperTest extends AndroidTestCase {
    +    private Contacts contacts;
    +    private Contacts mockContacts;
    +
    +    @Override
    +    public void setUp() throws Exception {
    +        super.setUp();
    +        contacts = new Contacts(getContext());
    +        mockContacts = new Contacts(getContext()) {
    +            @Override public String getNameForAddress(String address) {
    +                if ("test@testor.com".equals(address)) {
    +                    return "Tim Testor";
    +                } else {
    +                    return null;
    +                }
    +            }
    +        };
    +    }
    +
    +    public void testToFriendlyShowsPersonalPartIfItExists() throws Exception {
    +        Address address = new Address("test@testor.com", "Tim Testor");
    +        assertEquals("Tim Testor", MessageHelper.toFriendly(address, contacts));
    +    }
    +
    +    public void testToFriendlyShowsEmailPartIfNoPersonalPartExists() throws Exception {
    +        Address address = new Address("test@testor.com");
    +        assertEquals("test@testor.com", MessageHelper.toFriendly(address, contacts));
    +    }
    +
    +    public void testToFriendlyArray() throws Exception {
    +        Address address1 = new Address("test@testor.com", "Tim Testor");
    +        Address address2 = new Address("foo@bar.com", "Foo Bar");
    +        Address[] addresses = new Address[] { address1, address2 };
    +        assertEquals("Tim Testor,Foo Bar", MessageHelper.toFriendly(addresses, contacts).toString());
    +    }
    +
    +    public void testToFriendlyWithContactLookup() throws Exception {
    +        Address address = new Address("test@testor.com");
    +        assertEquals("Tim Testor", MessageHelper.toFriendly(address, mockContacts).toString());
    +    }
    +
    +    public void testToFriendlyWithChangeContactColor() throws Exception {
    +        Address address = new Address("test@testor.com");
    +        CharSequence friendly = MessageHelper.toFriendly(address, mockContacts, true, true, Color.RED);
    +        assertTrue(friendly instanceof SpannableString);
    +        assertEquals("Tim Testor", friendly.toString());
    +    }
    +
    +    public void testToFriendlyWithoutCorrespondentNames() throws Exception {
    +        Address address = new Address("test@testor.com", "Tim Testor");
    +        CharSequence friendly = MessageHelper.toFriendly(address, mockContacts, false, false, 0);
    +        assertEquals("test@testor.com", friendly.toString());
    +    }
    +}
    diff --git a/tests/src/com/fsck/k9/helper/Address.java b/tests/src/com/fsck/k9/mail/AddressTest.java
    similarity index 74%
    rename from tests/src/com/fsck/k9/helper/Address.java
    rename to tests/src/com/fsck/k9/mail/AddressTest.java
    index 250cad347..e1a3778e1 100644
    --- a/tests/src/com/fsck/k9/helper/Address.java
    +++ b/tests/src/com/fsck/k9/mail/AddressTest.java
    @@ -1,14 +1,14 @@
    -package com.fsck.k9.helper;
    +package com.fsck.k9.mail;
     import junit.framework.TestCase;
     
    -public class Address extends TestCase {
    +public class AddressTest extends TestCase {
         /**
          * test the possibility to parse "From:" fields with no email.
          * for example: From: News for Vector Limited - Google Finance
          * http://code.google.com/p/k9mail/issues/detail?id=3814
          */
         public void testParseWithMissingEmail() {
    -        com.fsck.k9.mail.Address[] addresses =  com.fsck.k9.mail.Address.parse("NAME ONLY");
    +        Address[] addresses =  Address.parse("NAME ONLY");
             assertEquals(1, addresses.length);
             assertEquals(null, addresses[0].getAddress());
             assertEquals("NAME ONLY", addresses[0].getPersonal());
    @@ -18,7 +18,7 @@ public class Address extends TestCase {
          * test name + valid email
          */
         public void testPraseWithValidEmail() {
    -        com.fsck.k9.mail.Address[] addresses =  com.fsck.k9.mail.Address.parse("Max Mustermann ");
    +        Address[] addresses =  Address.parse("Max Mustermann ");
             assertEquals(1, addresses.length);
             assertEquals("maxmuster@mann.com", addresses[0].getAddress());
             assertEquals("Max Mustermann", addresses[0].getPersonal());
    @@ -27,7 +27,7 @@ public class Address extends TestCase {
          * test with multi email addresses
          */
         public void testPraseWithValidEmailMulti() {
    -        com.fsck.k9.mail.Address[] addresses =  com.fsck.k9.mail.Address.parse("lorem@ipsum.us,mark@twain.com");
    +        Address[] addresses =  Address.parse("lorem@ipsum.us,mark@twain.com");
             assertEquals(2, addresses.length);
             assertEquals("lorem@ipsum.us", addresses[0].getAddress());
             assertEquals(null, addresses[0].getPersonal());
    diff --git a/tests/src/com/fsck/k9/helper/Utility_quoteAtoms.java b/tests/src/com/fsck/k9/mail/Address_quoteAtoms.java
    similarity index 87%
    rename from tests/src/com/fsck/k9/helper/Utility_quoteAtoms.java
    rename to tests/src/com/fsck/k9/mail/Address_quoteAtoms.java
    index 372c5c0f4..0ea553b2e 100644
    --- a/tests/src/com/fsck/k9/helper/Utility_quoteAtoms.java
    +++ b/tests/src/com/fsck/k9/mail/Address_quoteAtoms.java
    @@ -1,8 +1,8 @@
    -package com.fsck.k9.helper;
    +package com.fsck.k9.mail;
     
     import junit.framework.TestCase;
     
    -public class Utility_quoteAtoms extends TestCase
    +public class Address_quoteAtoms extends TestCase
     {
         public void testNoQuote() {
             // Alpha
    @@ -53,10 +53,10 @@ public class Utility_quoteAtoms extends TestCase
         }
     
         private void noQuote(final String s) {
    -        assertEquals(s, Utility.quoteAtoms(s));
    +        assertEquals(s, Address.quoteAtoms(s));
         }
     
         private String quote(final String s) {
    -        return Utility.quoteAtoms(s);
    +        return Address.quoteAtoms(s);
         }
     }
    diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java
    index 3e56b228d..70d771df4 100644
    --- a/tests/src/com/fsck/k9/mail/MessageTest.java
    +++ b/tests/src/com/fsck/k9/mail/MessageTest.java
    @@ -5,6 +5,9 @@ import java.io.ByteArrayOutputStream;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.OutputStream;
    +import java.util.Date;
    +import java.util.TimeZone;
    +
     import org.apache.commons.io.IOUtils;
     import org.apache.james.mime4j.codec.Base64InputStream;
     import org.apache.james.mime4j.util.MimeUtil;
    @@ -14,14 +17,19 @@ import android.test.AndroidTestCase;
     import com.fsck.k9.mail.Message.RecipientType;
     import com.fsck.k9.mail.internet.BinaryTempFileBody;
     import com.fsck.k9.mail.internet.BinaryTempFileMessageBody;
    +import com.fsck.k9.mail.internet.CharsetSupport;
     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;
     
     public class MessageTest extends AndroidTestCase {
    +    @Override
    +    public void setUp() throws Exception {
    +        super.setUp();
    +        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
    +    }
     
         private static final String EIGHT_BIT_RESULT =
                   "From: from@example.com\r\n"
    @@ -263,6 +271,28 @@ public class MessageTest extends AndroidTestCase {
             super();
         }
     
    +    public void testSetSendDateSetsSentDate() throws Exception {
    +        Message message = sampleMessage();
    +        final int milliseconds = 0;
    +        Date date = new Date(milliseconds);
    +        message.setSentDate(date, false);
    +        Date sentDate = message.getSentDate();
    +        assertNotNull(sentDate);
    +        assertEquals(milliseconds, sentDate.getTime());
    +    }
    +
    +    public void testSetSendDateFormatsHeaderCorrectlyWithCurrentTimeZone() throws Exception {
    +        Message message = sampleMessage();
    +        message.setSentDate(new Date(0), false);
    +        assertEquals("Thu, 01 Jan 1970 09:00:00 +0900", message.getHeader("Date")[0]);
    +    }
    +
    +    public void testSetSendDateFormatsHeaderCorrectlyWithoutTimeZone() throws Exception {
    +        Message message = sampleMessage();
    +        message.setSentDate(new Date(0), true);
    +        assertEquals("Thu, 01 Jan 1970 00:00:00 +0000", message.getHeader("Date")[0]);
    +    }
    +
         public void testMessage() throws MessagingException, IOException {
             MimeMessage message;
             ByteArrayOutputStream out;
    @@ -356,7 +386,7 @@ public class MessageTest extends AndroidTestCase {
                     + "End of test.\r\n");
             textBody.setCharset("utf-8");
             MimeBodyPart bodyPart = new MimeBodyPart(textBody, "text/plain");
    -        MimeUtility.setCharset("utf-8", bodyPart);
    +        CharsetSupport.setCharset("utf-8", bodyPart);
             bodyPart.setEncoding(encoding);
             return bodyPart;
         }
    @@ -374,7 +404,5 @@ public class MessageTest extends AndroidTestCase {
                 sb.append(Integer.toString(mMimeBoundary++));
                 return sb.toString();
             }
    -
         }
    -
     }
    diff --git a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java b/tests/src/com/fsck/k9/mail/ssl/TrustManagerFactoryTest.java
    similarity index 99%
    rename from tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java
    rename to tests/src/com/fsck/k9/mail/ssl/TrustManagerFactoryTest.java
    index 45efc0177..1574aaf03 100644
    --- a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java
    +++ b/tests/src/com/fsck/k9/mail/ssl/TrustManagerFactoryTest.java
    @@ -1,9 +1,7 @@
    -package com.fsck.k9.net.ssl;
    +package com.fsck.k9.mail.ssl;
     
     import javax.net.ssl.X509TrustManager;
     
    -import com.fsck.k9.security.LocalKeyStore;
    -
     import java.io.ByteArrayInputStream;
     import java.io.File;
     import java.security.cert.CertificateException;
    diff --git a/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java b/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
    index 92ef27dda..7badc97e0 100644
    --- a/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
    +++ b/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
    @@ -6,14 +6,13 @@ import java.util.Map;
     import com.fsck.k9.mail.AuthType;
     import com.fsck.k9.mail.ConnectionSecurity;
     import com.fsck.k9.mail.ServerSettings;
    -import com.fsck.k9.mail.Store;
    -import com.fsck.k9.mail.store.ImapStore;
    +
     import junit.framework.TestCase;
     
     public class ImapStoreUriTest extends TestCase {
         public void testDecodeStoreUriImapAllExtras() {
             String uri = "imap://PLAIN:user:pass@server:143/0%7CcustomPathPrefix";
    -        ServerSettings settings = Store.decodeStoreUri(uri);
    +        ServerSettings settings = RemoteStore.decodeStoreUri(uri);
     
             assertEquals(AuthType.PLAIN, settings.authenticationType);
             assertEquals("user", settings.username);
    @@ -26,7 +25,7 @@ public class ImapStoreUriTest extends TestCase {
     
         public void testDecodeStoreUriImapNoExtras() {
             String uri = "imap://PLAIN:user:pass@server:143/";
    -        ServerSettings settings = Store.decodeStoreUri(uri);
    +        ServerSettings settings = RemoteStore.decodeStoreUri(uri);
     
             assertEquals(AuthType.PLAIN, settings.authenticationType);
             assertEquals("user", settings.username);
    @@ -38,7 +37,7 @@ public class ImapStoreUriTest extends TestCase {
     
         public void testDecodeStoreUriImapPrefixOnly() {
             String uri = "imap://PLAIN:user:pass@server:143/customPathPrefix";
    -        ServerSettings settings = Store.decodeStoreUri(uri);
    +        ServerSettings settings = RemoteStore.decodeStoreUri(uri);
     
             assertEquals(AuthType.PLAIN, settings.authenticationType);
             assertEquals("user", settings.username);
    @@ -51,7 +50,7 @@ public class ImapStoreUriTest extends TestCase {
     
         public void testDecodeStoreUriImapEmptyPrefix() {
             String uri = "imap://PLAIN:user:pass@server:143/0%7C";
    -        ServerSettings settings = Store.decodeStoreUri(uri);
    +        ServerSettings settings = RemoteStore.decodeStoreUri(uri);
     
             assertEquals(AuthType.PLAIN, settings.authenticationType);
             assertEquals("user", settings.username);
    @@ -64,7 +63,7 @@ public class ImapStoreUriTest extends TestCase {
     
         public void testDecodeStoreUriImapAutodetectAndPrefix() {
             String uri = "imap://PLAIN:user:pass@server:143/1%7CcustomPathPrefix";
    -        ServerSettings settings = Store.decodeStoreUri(uri);
    +        ServerSettings settings = RemoteStore.decodeStoreUri(uri);
     
             assertEquals(AuthType.PLAIN, settings.authenticationType);
             assertEquals("user", settings.username);
    @@ -84,7 +83,7 @@ public class ImapStoreUriTest extends TestCase {
             ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
                     ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
     
    -        String uri = Store.createStoreUri(settings);
    +        String uri = RemoteStore.createStoreUri(settings);
     
             assertEquals("imap://PLAIN:user:pass@server:143/0%7CcustomPathPrefix", uri);
         }
    @@ -97,7 +96,7 @@ public class ImapStoreUriTest extends TestCase {
             ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
                     ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
     
    -        String uri = Store.createStoreUri(settings);
    +        String uri = RemoteStore.createStoreUri(settings);
     
             assertEquals("imap://PLAIN:user:pass@server:143/0%7C", uri);
         }
    @@ -106,7 +105,7 @@ public class ImapStoreUriTest extends TestCase {
             ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
                     ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null);
     
    -        String uri = Store.createStoreUri(settings);
    +        String uri = RemoteStore.createStoreUri(settings);
     
             assertEquals("imap://PLAIN:user:pass@server:143/1%7C", uri);
         }
    @@ -118,7 +117,7 @@ public class ImapStoreUriTest extends TestCase {
             ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
                     ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
     
    -        String uri = Store.createStoreUri(settings);
    +        String uri = RemoteStore.createStoreUri(settings);
     
             assertEquals("imap://PLAIN:user:pass@server:143/1%7C", uri);
         }
    diff --git a/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java b/tests/src/com/fsck/k9/mail/store/ImapUtilityTest.java
    similarity index 99%
    rename from tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java
    rename to tests/src/com/fsck/k9/mail/store/ImapUtilityTest.java
    index feaef05e8..392fdc554 100644
    --- a/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java
    +++ b/tests/src/com/fsck/k9/mail/store/ImapUtilityTest.java
    @@ -15,10 +15,11 @@
      * limitations under the License.
      */
     
    -package com.fsck.k9.mail.store.imap;
    +package com.fsck.k9.mail.store;
     
     import java.util.List;
     import android.test.MoreAsserts;
    +
     import junit.framework.TestCase;
     
     public class ImapUtilityTest extends TestCase {
    diff --git a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java b/tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
    similarity index 88%
    rename from tests/src/com/fsck/k9/mail/internet/ViewablesTest.java
    rename to tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
    index cceb4bff5..a6b417fa5 100644
    --- a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java
    +++ b/tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
    @@ -1,4 +1,4 @@
    -package com.fsck.k9.mail.internet;
    +package com.fsck.k9.mailstore;
     
     import java.util.Date;
     import java.util.Locale;
    @@ -9,9 +9,14 @@ import com.fsck.k9.activity.K9ActivityCommon;
     import com.fsck.k9.mail.Address;
     import com.fsck.k9.mail.MessagingException;
     import com.fsck.k9.mail.Message.RecipientType;
    -import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer;
    +import com.fsck.k9.mail.internet.MimeBodyPart;
    +import com.fsck.k9.mail.internet.MimeMessage;
    +import com.fsck.k9.mail.internet.MimeMultipart;
    +import com.fsck.k9.mail.internet.TextBody;
     
    -public class ViewablesTest extends AndroidTestCase {
    +import static com.fsck.k9.mailstore.LocalMessageExtractor.extractTextAndAttachments;
    +
    +public class LocalMessageExtractorTest extends AndroidTestCase {
     
         public void testSimplePlainTextMessage() throws MessagingException {
             String bodyText = "K-9 Mail rocks :>";
    @@ -24,7 +29,7 @@ public class ViewablesTest extends AndroidTestCase {
             message.setBody(body);
     
             // Extract text
    -        ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
    +        ViewableContainer container = extractTextAndAttachments(getContext(), message);
     
             String expectedText = bodyText;
             String expectedHtml =
    @@ -48,7 +53,7 @@ public class ViewablesTest extends AndroidTestCase {
             message.setBody(body);
     
             // Extract text
    -        ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
    +        ViewableContainer container = extractTextAndAttachments(getContext(), message);
     
             String expectedText = "K-9 Mail rocks :>";
             String expectedHtml =
    @@ -78,7 +83,7 @@ public class ViewablesTest extends AndroidTestCase {
             message.setBody(multipart);
     
             // Extract text
    -        ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
    +        ViewableContainer container = extractTextAndAttachments(getContext(), message);
     
             String expectedText =
                     bodyText1 + "\r\n\r\n" +
    @@ -115,7 +120,7 @@ public class ViewablesTest extends AndroidTestCase {
     
             // Create message/rfc822 body
             MimeMessage innerMessage = new MimeMessage();
    -        innerMessage.addSentDate(new Date(112, 02, 17));
    +        innerMessage.addSentDate(new Date(112, 02, 17), false);
             innerMessage.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") });
             innerMessage.setSubject("Subject");
             innerMessage.setFrom(new Address("from@example.com"));
    @@ -134,7 +139,7 @@ public class ViewablesTest extends AndroidTestCase {
             message.setBody(multipart);
     
             // Extract text
    -        ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
    +        ViewableContainer container = extractTextAndAttachments(getContext(), message);
     
             String expectedText =
                     bodyText +