diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 29ff20015..287de6eee 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -67,7 +67,7 @@ @@ -75,6 +75,12 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/menu/accounts_context.xml b/res/menu/accounts_context.xml index 798d32d20..9eb952915 100644 --- a/res/menu/accounts_context.xml +++ b/res/menu/accounts_context.xml @@ -21,6 +21,8 @@ android:title="@string/remove_account_action" /> + + + + + + + diff --git a/res/menu/disabled_accounts_context.xml b/res/menu/disabled_accounts_context.xml new file mode 100644 index 000000000..54d6c2afc --- /dev/null +++ b/res/menu/disabled_accounts_context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index f1e86ad46..20cc791c5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1051,6 +1051,46 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Unable to connect. + Settings Import & Export + Export account settings + Export settings and accounts + Import + Export + Import settings + Import selection + Global settings + Exporting settings... + Importing settings... + Scanning file... + Saved exported settings to %s + Imported global settings from %s + Imported %s from %s + + 1 account + %s accounts + + Failed to export settings + Failed to import any settings from %s + Export succeeded + Export failed + Import succeeded + Import failed + Activate account + To be able to use the account \"%s\" you need to provide the %s. + + server password + server passwords + + Incoming server (%s): + Outgoing server (%s): + + Setting password... + Setting passwords... + + Activate + + Unable to handle file of version %s + Account \"%s\" is unavailable; check storage Save attachments to... @@ -1059,5 +1099,6 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Move up Move down + Moving account... diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index f81fe794f..3a891c286 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -59,12 +59,21 @@ public class Account implements BaseAccount { public static final String TYPE_OTHER = "OTHER"; private static final String[] networkTypes = { TYPE_WIFI, TYPE_MOBILE, TYPE_OTHER }; - private static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML; - private static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false; - private static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX; - private static final String DEFAULT_QUOTE_PREFIX = ">"; - private static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true; - private static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; + public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML; + public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false; + public static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX; + public static final String DEFAULT_QUOTE_PREFIX = ">"; + public static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true; + public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; + + public static final String ACCOUNT_DESCRIPTION_KEY = "description"; + public static final String STORE_URI_KEY = "storeUri"; + public static final String TRANSPORT_URI_KEY = "transportUri"; + + public static final String IDENTITY_NAME_KEY = "name"; + public static final String IDENTITY_EMAIL_KEY = "email"; + public static final String IDENTITY_DESCRIPTION_KEY = "description"; + /** *
@@ -139,6 +148,16 @@ public class Account implements BaseAccount {
 
     private CryptoProvider mCryptoProvider = null;
 
+    /**
+     * Indicates whether this account is enabled, i.e. ready for use, or not.
+     *
+     * 

+ * Right now newly imported accounts are disabled if the settings file didn't contain a + * password for the incoming and/or outgoing server. + *

+ */ + private boolean mEnabled; + /** * Name of the folder that was last selected for a copy or move operation. * @@ -215,6 +234,7 @@ public class Account implements BaseAccount { mSyncRemoteDeletions = true; mCryptoApp = Apg.NAME; mCryptoAutoSignature = false; + mEnabled = true; searchableFolders = Searchable.ALL; @@ -377,8 +397,9 @@ public class Account implements BaseAccount { mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME); mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false); + mEnabled = prefs.getBoolean(mUuid + ".enabled", true); } - + protected synchronized void delete(Preferences preferences) { String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(","); String[] newUuids = new String[uuids.length - 1]; @@ -388,7 +409,7 @@ public class Account implements BaseAccount { newUuids[i++] = uuid; } } - + String accountUuids = Utility.combine(newUuids, ','); SharedPreferences.Editor editor = preferences.getPreferences().edit(); editor.putString("accountUuids", accountUuids); @@ -446,6 +467,7 @@ public class Account implements BaseAccount { editor.remove(mUuid + ".replyAfterQuote"); editor.remove(mUuid + ".cryptoApp"); editor.remove(mUuid + ".cryptoAutoSignature"); + editor.remove(mUuid + ".enabled"); editor.remove(mUuid + ".enableMoveButtons"); editor.remove(mUuid + ".hideMoveButtonsEnum"); for (String type : networkTypes) { @@ -480,7 +502,7 @@ public class Account implements BaseAccount { List accountNumbers = getExistingAccountNumbers(preferences); return findNewAccountNumber(accountNumbers); } - + public void move(Preferences preferences, boolean moveUp) { String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(","); SharedPreferences.Editor editor = preferences.getPreferences().edit(); @@ -489,7 +511,7 @@ public class Account implements BaseAccount { for (int i = 0; i < uuids.length; i++) { if (i > 0 && uuids[i].equals(mUuid)) { newUuids[i] = newUuids[i-1]; - newUuids[i-1] = mUuid; + newUuids[i-1] = mUuid; } else { newUuids[i] = uuids[i]; @@ -500,7 +522,7 @@ public class Account implements BaseAccount { for (int i = uuids.length - 1; i >= 0; i--) { if (i < uuids.length - 1 && uuids[i].equals(mUuid)) { newUuids[i] = newUuids[i+1]; - newUuids[i+1] = mUuid; + newUuids[i+1] = mUuid; } else { newUuids[i] = uuids[i]; @@ -510,9 +532,9 @@ public class Account implements BaseAccount { String accountUuids = Utility.combine(newUuids, ','); editor.putString("accountUuids", accountUuids); editor.commit(); - preferences.refreshAccounts(); + preferences.loadAccounts(); } - + public synchronized void save(Preferences preferences) { SharedPreferences.Editor editor = preferences.getPreferences().edit(); @@ -598,6 +620,7 @@ public class Account implements BaseAccount { editor.putBoolean(mUuid + ".replyAfterQuote", mReplyAfterQuote); editor.putString(mUuid + ".cryptoApp", mCryptoApp); editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature); + editor.putBoolean(mUuid + ".enabled", mEnabled); editor.putBoolean(mUuid + ".vibrate", mNotificationSetting.shouldVibrate()); editor.putInt(mUuid + ".vibratePattern", mNotificationSetting.getVibratePattern()); @@ -1096,18 +1119,17 @@ public class Account implements BaseAccount { return mUuid.hashCode(); } - private synchronized List loadIdentities(SharedPreferences prefs) { List newIdentities = new ArrayList(); int ident = 0; boolean gotOne = false; do { gotOne = false; - String name = prefs.getString(mUuid + ".name." + ident, null); - String email = prefs.getString(mUuid + ".email." + ident, null); + String name = prefs.getString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, null); + String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null); boolean signatureUse = prefs.getBoolean(mUuid + ".signatureUse." + ident, true); String signature = prefs.getString(mUuid + ".signature." + ident, null); - String description = prefs.getString(mUuid + ".description." + ident, null); + String description = prefs.getString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, null); final String replyTo = prefs.getString(mUuid + ".replyTo." + ident, null); if (email != null) { Identity identity = new Identity(); @@ -1145,13 +1167,13 @@ public class Account implements BaseAccount { boolean gotOne = false; do { gotOne = false; - String email = prefs.getString(mUuid + ".email." + ident, null); + String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null); if (email != null) { - editor.remove(mUuid + ".name." + ident); - editor.remove(mUuid + ".email." + ident); + editor.remove(mUuid + "." + IDENTITY_NAME_KEY + "." + ident); + editor.remove(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident); editor.remove(mUuid + ".signatureUse." + ident); editor.remove(mUuid + ".signature." + ident); - editor.remove(mUuid + ".description." + ident); + editor.remove(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident); editor.remove(mUuid + ".replyTo." + ident); gotOne = true; } @@ -1164,11 +1186,11 @@ public class Account implements BaseAccount { int ident = 0; for (Identity identity : identities) { - editor.putString(mUuid + ".name." + ident, identity.getName()); - editor.putString(mUuid + ".email." + ident, identity.getEmail()); + editor.putString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, identity.getName()); + editor.putString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, identity.getEmail()); editor.putBoolean(mUuid + ".signatureUse." + ident, identity.getSignatureUse()); editor.putString(mUuid + ".signature." + ident, identity.getSignature()); - editor.putString(mUuid + ".description." + ident, identity.getDescription()); + editor.putString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, identity.getDescription()); editor.putString(mUuid + ".replyTo." + ident, identity.getReplyTo()); ident++; } @@ -1460,4 +1482,11 @@ public class Account implements BaseAccount { return StorageManager.getInstance(K9.app).isReady(localStorageProviderId); } + public synchronized boolean isEnabled() { + return mEnabled; + } + + public synchronized void setEnabled(boolean enabled) { + mEnabled = enabled; + } } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index fd43cfc6a..6b8212295 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -463,59 +463,7 @@ public class K9 extends Application { galleryBuggy = checkForBuggyGallery(); Preferences prefs = Preferences.getPreferences(this); - SharedPreferences sprefs = prefs.getPreferences(); - DEBUG = sprefs.getBoolean("enableDebugLogging", false); - DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); - mAnimations = sprefs.getBoolean("animations", true); - mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true); - mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); - mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false); - mManageBack = sprefs.getBoolean("manageBack", false); - mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false); - mMeasureAccounts = sprefs.getBoolean("measureAccounts", true); - mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true); - mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false); - mMessageListStars = sprefs.getBoolean("messageListStars", true); - mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false); - mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false); - mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2); - - mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false); - mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false); - - mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false); - mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00"); - mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00"); - - mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true); - mShowContactName = sprefs.getBoolean("showContactName", false); - mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false); - mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f); - mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false); - mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false); - mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false); - - useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy()); - - mConfirmDelete = sprefs.getBoolean("confirmDelete", false); - mConfirmSpam = sprefs.getBoolean("confirmSpam", false); - mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true); - - - mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false); - - compactLayouts = sprefs.getBoolean("compactLayouts", false); - mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); - fontSizes.load(sprefs); - - try { - setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED"))); - } catch (Exception e) { - setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED); - } - - K9.setK9Language(sprefs.getString("language", "")); - K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); + loadPrefs(prefs); /* * We have to give MimeMessage a temp directory because File.createTempFile(String, String) @@ -587,6 +535,62 @@ public class K9 extends Application { notifyObservers(); } + public static void loadPrefs(Preferences prefs) { + SharedPreferences sprefs = prefs.getPreferences(); + DEBUG = sprefs.getBoolean("enableDebugLogging", false); + DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); + mAnimations = sprefs.getBoolean("animations", true); + mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true); + mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); + mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false); + mManageBack = sprefs.getBoolean("manageBack", false); + mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false); + mMeasureAccounts = sprefs.getBoolean("measureAccounts", true); + mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true); + mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false); + mMessageListStars = sprefs.getBoolean("messageListStars", true); + mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false); + mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false); + mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2); + + mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false); + mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false); + + mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false); + mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00"); + mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00"); + + mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true); + mShowContactName = sprefs.getBoolean("showContactName", false); + mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false); + mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f); + mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false); + mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false); + mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false); + + useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy()); + + mConfirmDelete = sprefs.getBoolean("confirmDelete", false); + mConfirmSpam = sprefs.getBoolean("confirmSpam", false); + mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true); + + + mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false); + + compactLayouts = sprefs.getBoolean("compactLayouts", false); + mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); + fontSizes.load(sprefs); + + try { + setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED"))); + } catch (Exception e) { + setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED); + } + + K9.setK9Language(sprefs.getString("language", "")); + K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); + } + private void maybeSetupStrictMode() { if (!K9.DEVELOPER_MODE) return; diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index f30c7e578..cbaa8623b 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -49,36 +49,23 @@ public class Preferences { } } - private synchronized void loadAccounts() { + public synchronized void loadAccounts() { accounts = new HashMap(); - refreshAccounts(); - } - - public synchronized void refreshAccounts() { - Map newAccountMap = new HashMap(); accountsInOrder = new LinkedList(); String accountUuids = getPreferences().getString("accountUuids", null); if ((accountUuids != null) && (accountUuids.length() != 0)) { String[] uuids = accountUuids.split(","); for (String uuid : uuids) { - Account account = accounts.get(uuid); - if (account != null) { - newAccountMap.put(uuid, account); - accountsInOrder.add(account); - } else { - Account newAccount = new Account(this, uuid); - newAccountMap.put(uuid, newAccount); - accountsInOrder.add(newAccount); - } + Account newAccount = new Account(this, uuid); + accounts.put(uuid, newAccount); + accountsInOrder.add(newAccount); } } if ((newAccount != null) && newAccount.getAccountNumber() != -1) { - newAccountMap.put(newAccount.getUuid(), newAccount); + accounts.put(newAccount.getUuid(), newAccount); accountsInOrder.add(newAccount); newAccount = null; } - - accounts = newAccountMap; } /** @@ -103,7 +90,7 @@ public class Preferences { Account[] allAccounts = getAccounts(); Collection retval = new ArrayList(accounts.size()); for (Account account : allAccounts) { - if (account.isAvailable(mContext)) { + if (account.isEnabled() && account.isAvailable(mContext)) { retval.add(account); } } @@ -129,12 +116,12 @@ public class Preferences { } public synchronized void deleteAccount(Account account) { - if (accounts != null) { - accounts.remove(account.getUuid()); - } - if (accountsInOrder != null) { - accountsInOrder.remove(account); - } + if (accounts != null) { + accounts.remove(account.getUuid()); + } + if (accountsInOrder != null) { + accountsInOrder.remove(account); + } account.delete(this); diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 4bc0d3291..45c622ee5 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -1,6 +1,9 @@ package com.fsck.k9.activity; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; @@ -10,16 +13,24 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import android.app.Activity; import android.app.AlertDialog; +import android.app.Application; import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; +import android.util.SparseBooleanArray; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Menu; @@ -32,14 +43,22 @@ import android.view.View.OnClickListener; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CheckedTextView; +import android.widget.CompoundButton; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.CompoundButton.OnCheckedChangeListener; import com.fsck.k9.Account; import com.fsck.k9.AccountStats; @@ -50,6 +69,8 @@ import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.SearchAccount; import com.fsck.k9.SearchSpecification; +import com.fsck.k9.activity.misc.ExtendedAsyncTask; +import com.fsck.k9.activity.misc.NonConfigurationInstance; import com.fsck.k9.activity.setup.AccountSettings; import com.fsck.k9.activity.setup.AccountSetupBasics; import com.fsck.k9.activity.setup.Prefs; @@ -57,8 +78,21 @@ import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.SizeFormatter; import com.fsck.k9.mail.Flag; +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.WebDavStore; import com.fsck.k9.view.ColorChip; +import com.fsck.k9.preferences.SettingsExporter; +import com.fsck.k9.preferences.SettingsImportExportException; +import com.fsck.k9.preferences.SettingsImporter; +import com.fsck.k9.preferences.SettingsImporter.AccountDescription; +import com.fsck.k9.preferences.SettingsImporter.AccountDescriptionPair; +import com.fsck.k9.preferences.SettingsImporter.ImportContents; +import com.fsck.k9.preferences.SettingsImporter.ImportResults; + public class Accounts extends K9ListActivity implements OnItemClickListener, OnClickListener { @@ -88,6 +122,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC private SearchAccount integratedInboxAccount = null; private FontSizes mFontSizes = K9.getFontSizes(); + /** + * Contains information about objects that need to be retained on configuration changes. + * + * @see #onRetainNonConfigurationInstance() + */ + private NonConfigurationInstance mNonConfigurationInstance; + + + private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1; + class AccountsHandler extends Handler { private void setViewTitle() { String dispString = mListener.formatHeader(Accounts.this, getString(R.string.accounts_title), mUnreadMessageCount, getTimeFormat()); @@ -158,6 +202,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } + public void setProgress(boolean progress) { + mHandler.progress(progress); + } + ActivityListener mListener = new ActivityListener() { @Override public void informUserOfStatus() { @@ -256,6 +304,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC context.startActivity(intent); } + @Override + public void onNewIntent(Intent intent) { + Uri uri = intent.getData(); + Log.i(K9.LOG_TAG, "Accounts Activity got uri " + uri); + if (uri != null) { + ContentResolver contentResolver = getContentResolver(); + + Log.i(K9.LOG_TAG, "Accounts Activity got content of type " + contentResolver.getType(uri)); + + String contentType = contentResolver.getType(uri); + if (MimeUtility.K9_SETTINGS_MIME_TYPE.equals(contentType)) { + onImport(uri); + } + } + } @Override public void onCreate(Bundle icicle) { @@ -273,32 +336,41 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC Account[] accounts = Preferences.getPreferences(this).getAccounts(); Intent intent = getIntent(); + //onNewIntent(intent); + boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true); if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) { onOpenAccount(integratedInboxAccount); finish(); + return; } else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) { - // fall through to "else" if !onOpenAccount() finish(); - } else { - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - requestWindowFeature(Window.FEATURE_PROGRESS); + return; + } - setContentView(R.layout.accounts); - ListView listView = getListView(); - listView.setOnItemClickListener(this); - listView.setItemsCanFocus(false); - listView.setEmptyView(findViewById(R.id.empty)); - listView.setScrollingCacheEnabled(false); - findViewById(R.id.next).setOnClickListener(this); - registerForContextMenu(listView); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); - if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) { - String accountUuid = icicle.getString("selectedContextAccount"); - mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid); - } + setContentView(R.layout.accounts); + ListView listView = getListView(); + listView.setOnItemClickListener(this); + listView.setItemsCanFocus(false); + listView.setEmptyView(findViewById(R.id.empty)); + listView.setScrollingCacheEnabled(false); + findViewById(R.id.next).setOnClickListener(this); + registerForContextMenu(listView); - restoreAccountStats(icicle); + if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) { + String accountUuid = icicle.getString("selectedContextAccount"); + mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid); + } + + restoreAccountStats(icicle); + + // Handle activity restarts because of a configuration change (e.g. rotating the screen) + mNonConfigurationInstance = (NonConfigurationInstance) getLastNonConfigurationInstance(); + if (mNonConfigurationInstance != null) { + mNonConfigurationInstance.restore(this); } } @@ -348,9 +420,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC super.onPause(); MessagingController.getInstance(getApplication()).removeListener(mListener); StorageManager.getInstance(getApplication()).removeListener(storageListener); - } - + + /** + * Save the reference to a currently displayed dialog or a running AsyncTask (if available). + */ + @Override + public Object onRetainNonConfigurationInstance() { + Object retain = null; + if (mNonConfigurationInstance != null && mNonConfigurationInstance.retain()) { + retain = mNonConfigurationInstance; + } + return retain; + } + private BaseAccount[] accounts = new BaseAccount[0]; private enum ACCOUNT_LOCATION { TOP, MIDDLE, BOTTOM; @@ -365,12 +448,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC if (accounts[accounts.length - 1].equals(account)) { accountLocation.remove(ACCOUNT_LOCATION.MIDDLE); accountLocation.add(ACCOUNT_LOCATION.BOTTOM); - } + } } return accountLocation; } - - + + private void refresh() { accounts = Preferences.getPreferences(this).getAccounts(); @@ -472,7 +555,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount); } else { Account realAccount = (Account)account; - if (!realAccount.isAvailable(this)) { + if (!realAccount.isEnabled()) { + onActivateAccount(realAccount); + return false; + } else if (!realAccount.isAvailable(this)) { String toastText = getString(R.string.account_unavailable, account.getDescription()); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_SHORT); toast.show(); @@ -489,6 +575,319 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC return true; } + private void onActivateAccount(Account account) { + List disabledAccounts = new ArrayList(); + disabledAccounts.add(account); + promptForServerPasswords(disabledAccounts); + } + + /** + * Ask the user to enter the server passwords for disabled accounts. + * + * @param disabledAccounts + * A non-empty list of {@link Account}s to ask the user for passwords. Never + * {@code null}. + *

Note: Calling this method will modify the supplied list.

+ */ + private void promptForServerPasswords(final List disabledAccounts) { + Account account = disabledAccounts.remove(0); + PasswordPromptDialog dialog = new PasswordPromptDialog(account, disabledAccounts); + setNonConfigurationInstance(dialog); + dialog.show(this); + } + + /** + * Ask the user for the incoming/outgoing server passwords. + */ + private static class PasswordPromptDialog implements NonConfigurationInstance, TextWatcher { + private AlertDialog mDialog; + private EditText mIncomingPasswordView; + private EditText mOutgoingPasswordView; + private CheckBox mUseIncomingView; + + private Account mAccount; + private List mRemainingAccounts; + private String mIncomingPassword; + private String mOutgoingPassword; + private boolean mUseIncoming; + + /** + * Constructor + * + * @param account + * The {@link Account} to ask the server passwords for. Never {@code null}. + * @param accounts + * The (possibly empty) list of remaining accounts to ask passwords for. Never + * {@code null}. + */ + PasswordPromptDialog(Account account, List accounts) { + mAccount = account; + mRemainingAccounts = accounts; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity, true); + } + + @Override + public boolean retain() { + if (mDialog != null) { + // Retain entered passwords and checkbox state + mIncomingPassword = mIncomingPasswordView.getText().toString(); + if (mOutgoingPasswordView != null) { + mOutgoingPassword = mOutgoingPasswordView.getText().toString(); + mUseIncoming = mUseIncomingView.isChecked(); + } + + // Dismiss dialog + mDialog.dismiss(); + + // Clear all references to UI objects + mDialog = null; + mIncomingPasswordView = null; + mOutgoingPasswordView = null; + mUseIncomingView = null; + return true; + } + return false; + } + + public void show(Accounts activity) { + show(activity, false); + } + + private void show(final Accounts activity, boolean restore) { + ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri()); + ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri()); + + // Don't ask for the password to the outgoing server for WebDAV accounts, because + // incoming and outgoing servers are identical for this account type. + boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type); + + // Create a ScrollView that will be used as container for the whole layout + final ScrollView scrollView = new ScrollView(activity); + + // Create the dialog + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.settings_import_activate_account_header)); + builder.setView(scrollView); + builder.setPositiveButton(activity.getString(R.string.okay_action), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String incomingPassword = mIncomingPasswordView.getText().toString(); + String outgoingPassword = null; + if (mOutgoingPasswordView != null) { + outgoingPassword = (mUseIncomingView.isChecked()) ? + incomingPassword : mOutgoingPasswordView.getText().toString(); + } + + dialog.dismiss(); + + // Set the server passwords in the background + SetPasswordsAsyncTask asyncTask = new SetPasswordsAsyncTask(activity, mAccount, + incomingPassword, outgoingPassword, mRemainingAccounts); + activity.setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + }); + builder.setNegativeButton(activity.getString(R.string.cancel_action), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + } + }); + mDialog = builder.create(); + + // Use the dialog's layout inflater so its theme is used (and not the activity's theme). + View layout = mDialog.getLayoutInflater().inflate( + R.layout.accounts_password_prompt, null); + + // Set the intro text that tells the user what to do + TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro); + String serverPasswords = activity.getResources().getQuantityString( + R.plurals.settings_import_server_passwords, + (configureOutgoingServer) ? 2 : 1); + intro.setText(activity.getString(R.string.settings_import_activate_account_intro, + mAccount.getDescription(), serverPasswords)); + + // Display the hostname of the incoming server + TextView incomingText = (TextView) layout.findViewById( + R.id.password_prompt_incoming_server); + incomingText.setText(activity.getString(R.string.settings_import_incoming_server, + incoming.host)); + + mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password); + mIncomingPasswordView.addTextChangedListener(this); + + if (configureOutgoingServer) { + // Display the hostname of the outgoing server + TextView outgoingText = (TextView) layout.findViewById( + R.id.password_prompt_outgoing_server); + outgoingText.setText(activity.getString(R.string.settings_import_outgoing_server, + outgoing.host)); + + mOutgoingPasswordView = (EditText) layout.findViewById( + R.id.outgoing_server_password); + mOutgoingPasswordView.addTextChangedListener(this); + + mUseIncomingView = (CheckBox) layout.findViewById( + R.id.use_incoming_server_password); + mUseIncomingView.setChecked(true); + mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mOutgoingPasswordView.setText(null); + mOutgoingPasswordView.setEnabled(false); + } else { + mOutgoingPasswordView.setText(mIncomingPasswordView.getText()); + mOutgoingPasswordView.setEnabled(true); + } + } + }); + } else { + layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE); + } + + // Add the layout to the ScrollView + scrollView.addView(layout); + + // Show the dialog + mDialog.show(); + + // Restore the contents of the password boxes and the checkbox (if the dialog was + // retained during a configuration change). + if (restore) { + mIncomingPasswordView.setText(mIncomingPassword); + if (configureOutgoingServer) { + mOutgoingPasswordView.setText(mOutgoingPassword); + mUseIncomingView.setChecked(mUseIncoming); + } + } else { + // Trigger afterTextChanged() being called + // Work around this bug: https://code.google.com/p/android/issues/detail?id=6360 + mIncomingPasswordView.setText(mIncomingPasswordView.getText()); + } + } + + @Override + public void afterTextChanged(Editable arg0) { + boolean enable = false; + // Is the password box for the incoming server password empty? + if (mIncomingPasswordView.getText().length() > 0) { + // Do we need to check the outgoing server password box? + if (mOutgoingPasswordView == null) { + enable = true; + } + // If the checkbox to use the incoming server password is checked we need to make + // sure that the password box for the outgoing server isn't empty. + else if (mUseIncomingView.isChecked() || + mOutgoingPasswordView.getText().length() > 0) { + enable = true; + } + } + + // Disable "OK" button if the user hasn't specified all necessary passwords. + mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Not used + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Not used + } + } + + /** + * Set the incoming/outgoing server password in the background. + */ + private static class SetPasswordsAsyncTask extends ExtendedAsyncTask { + private Account mAccount; + private String mIncomingPassword; + private String mOutgoingPassword; + private List mRemainingAccounts; + private Application mApplication; + + protected SetPasswordsAsyncTask(Activity activity, Account account, + String incomingPassword, String outgoingPassword, + List remainingAccounts) { + super(activity); + mAccount = account; + mIncomingPassword = incomingPassword; + mOutgoingPassword = outgoingPassword; + mRemainingAccounts = remainingAccounts; + mApplication = mActivity.getApplication(); + } + + @Override + protected void showProgressDialog() { + String title = mActivity.getString(R.string.settings_import_activate_account_header); + int passwordCount = (mOutgoingPassword == null) ? 1 : 2; + String message = mActivity.getResources().getQuantityString( + R.plurals.settings_import_setting_passwords, passwordCount); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Void doInBackground(Void... params) { + try { + // Set incoming server password + String storeUri = mAccount.getStoreUri(); + ServerSettings incoming = Store.decodeStoreUri(storeUri); + ServerSettings newIncoming = incoming.newPassword(mIncomingPassword); + String newStoreUri = Store.createStoreUri(newIncoming); + mAccount.setStoreUri(newStoreUri); + + if (mOutgoingPassword != null) { + // Set outgoing server password + String transportUri = mAccount.getTransportUri(); + ServerSettings outgoing = Transport.decodeTransportUri(transportUri); + ServerSettings newOutgoing = outgoing.newPassword(mOutgoingPassword); + String newTransportUri = Transport.createTransportUri(newOutgoing); + mAccount.setTransportUri(newTransportUri); + } + + // Mark account as enabled + mAccount.setEnabled(true); + + // Save the account settings + mAccount.save(Preferences.getPreferences(mContext)); + + // Start services if necessary + K9.setServicesEnabled(mContext); + + // Get list of folders from remote server + MessagingController.getInstance(mApplication).listFolders(mAccount, true, null); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Something went while setting account passwords", e); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + activity.refresh(); + removeProgressDialog(); + + if (mRemainingAccounts.size() > 0) { + activity.promptForServerPasswords(mRemainingAccounts); + } + } + } + public void onClick(View view) { if (view.getId() == R.id.next) { onAddNewAccount(); @@ -613,6 +1012,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.open: onOpenAccount(mSelectedContextAccount); break; + case R.id.activate: + onActivateAccount(realAccount); + break; case R.id.check_mail: onCheckMail(realAccount); break; @@ -631,6 +1033,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.recreate: onRecreate(realAccount); break; + case R.id.export: + onExport(false, realAccount); + break; case R.id.move_up: onMove(realAccount, true); break; @@ -656,22 +1061,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC showDialog(DIALOG_RECREATE_ACCOUNT); } private void onMove(final Account account, final boolean up) { - mHandler.progress(true); - AsyncUIProcessor.getInstance(getApplication()).execute( - new Runnable() - { - @Override - public void run() { - account.move(Preferences.getPreferences(Accounts.this), up); - runOnUiThread(new Runnable() { - @Override - public void run() { - refresh(); - mHandler.progress(false); - } - }); - } - }); + MoveAccountAsyncTask asyncTask = new MoveAccountAsyncTask(this, account, up); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); } public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -700,6 +1092,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.search: onSearchRequested(); break; + case R.id.export_all: + onExport(true, null); + break; + case R.id.import_settings: + onImport(); + break; default: return super.onOptionsItemSelected(item); } @@ -800,10 +1198,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.setHeaderTitle(R.string.accounts_context_menu_title); - getMenuInflater().inflate(R.menu.accounts_context, menu); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; BaseAccount account = mAdapter.getItem(info.position); + + if ((account instanceof Account) && !((Account) account).isEnabled()) { + getMenuInflater().inflate(R.menu.disabled_accounts_context, menu); + } else { + getMenuInflater().inflate(R.menu.accounts_context, menu); + } + if (account instanceof SearchAccount) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); @@ -829,6 +1233,317 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } + private void onImport() { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType(MimeUtility.K9_SETTINGS_MIME_TYPE); + startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.i(K9.LOG_TAG, "onActivityResult requestCode = " + requestCode + ", resultCode = " + resultCode + ", data = " + data); + if (resultCode != RESULT_OK) + return; + if (data == null) { + return; + } + switch (requestCode) { + case ACTIVITY_REQUEST_PICK_SETTINGS_FILE: + onImport(data.getData()); + break; + } + } + + private void onImport(Uri uri) { + ListImportContentsAsyncTask asyncTask = new ListImportContentsAsyncTask(this, uri); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + + + private void showSimpleDialog(int headerRes, int messageRes, Object... args) { + SimpleDialog dialog = new SimpleDialog(headerRes, messageRes, args); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A simple dialog. + */ + private static class SimpleDialog implements NonConfigurationInstance { + private final int mHeaderRes; + private final int mMessageRes; + private Object[] mArguments; + private Dialog mDialog; + + SimpleDialog(int headerRes, int messageRes, Object... args) { + this.mHeaderRes = headerRes; + this.mMessageRes = messageRes; + this.mArguments = args; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity); + } + + @Override + public boolean retain() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + return true; + } + return false; + } + + public void show(final Accounts activity) { + final String message = generateMessage(activity); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(mHeaderRes); + builder.setMessage(message); + builder.setPositiveButton(R.string.okay_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + okayAction(activity); + } + }); + mDialog = builder.show(); + } + + /** + * Returns the message the dialog should display. + * + * @param activity + * The {@code Activity} this dialog belongs to. + * + * @return The message the dialog should display + */ + protected String generateMessage(Accounts activity) { + return activity.getString(mMessageRes, mArguments); + } + + /** + * This method is called after the "OK" button was pressed. + * + * @param activity + * The {@code Activity} this dialog belongs to. + */ + protected void okayAction(Accounts activity) { + // Do nothing + } + } + + /** + * Shows a dialog that displays how many accounts were successfully imported. + * + * @param importResults + * The {@link ImportResults} instance returned by the {@link SettingsImporter}. + * @param filename + * The name of the settings file that was imported. + */ + private void showAccountsImportedDialog(ImportResults importResults, String filename) { + AccountsImportedDialog dialog = new AccountsImportedDialog(importResults, filename); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A dialog that displays how many accounts were successfully imported. + */ + private static class AccountsImportedDialog extends SimpleDialog { + private ImportResults mImportResults; + private String mFilename; + + AccountsImportedDialog(ImportResults importResults, String filename) { + super(R.string.settings_import_success_header, R.string.settings_import_success); + mImportResults = importResults; + mFilename = filename; + } + + @Override + protected String generateMessage(Accounts activity) { + //TODO: display names of imported accounts (name from file *and* possibly new name) + + int imported = mImportResults.importedAccounts.size(); + String accounts = activity.getResources().getQuantityString( + R.plurals.settings_import_success, imported, imported); + return activity.getString(R.string.settings_import_success, accounts, mFilename); + } + + @Override + protected void okayAction(Accounts activity) { + Context context = activity.getApplicationContext(); + Preferences preferences = Preferences.getPreferences(context); + List disabledAccounts = new ArrayList(); + for (AccountDescriptionPair accountPair : mImportResults.importedAccounts) { + Account account = preferences.getAccount(accountPair.imported.uuid); + if (!account.isEnabled()) { + disabledAccounts.add(account); + } + } + activity.promptForServerPasswords(disabledAccounts); + } + } + + /** + * Display a dialog that lets the user select which accounts to import from the settings file. + * + * @param importContents + * The {@link ImportContents} instance returned by + * {@link SettingsImporter#getImportStreamContents(InputStream)} + * @param uri + * The (content) URI of the settings file. + */ + private void showImportSelectionDialog(ImportContents importContents, Uri uri) { + ImportSelectionDialog dialog = new ImportSelectionDialog(importContents, uri); + dialog.show(this); + setNonConfigurationInstance(dialog); + } + + /** + * A dialog that lets the user select which accounts to import from the settings file. + */ + private static class ImportSelectionDialog implements NonConfigurationInstance { + private ImportContents mImportContents; + private Uri mUri; + private Dialog mDialog; + private ListView mImportSelectionView; + private SparseBooleanArray mSelection; + + + ImportSelectionDialog(ImportContents importContents, Uri uri) { + mImportContents = importContents; + mUri = uri; + } + + @Override + public void restore(Activity activity) { + show((Accounts) activity, mSelection); + } + + @Override + public boolean retain() { + if (mDialog != null) { + // Save the selection state of each list item + mSelection = mImportSelectionView.getCheckedItemPositions(); + mImportSelectionView = null; + + mDialog.dismiss(); + mDialog = null; + return true; + } + return false; + } + + public void show(Accounts activity) { + show(activity, null); + } + + public void show(final Accounts activity, SparseBooleanArray selection) { + final ListView importSelectionView = new ListView(activity); + mImportSelectionView = importSelectionView; + List contents = new ArrayList(); + + if (mImportContents.globalSettings) { + contents.add(activity.getString(R.string.settings_import_global_settings)); + } + + for (AccountDescription account : mImportContents.accounts) { + contents.add(account.name); + } + + importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + importSelectionView.setAdapter(new ArrayAdapter(activity, + android.R.layout.simple_list_item_checked, contents)); + importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + CheckedTextView ctv = (CheckedTextView)view; + ctv.setChecked(!ctv.isChecked()); + } + + @Override + public void onNothingSelected(AdapterView arg0) { /* Do nothing */ } + }); + + if (selection != null) { + for (int i = 0, end = contents.size(); i < end; i++) { + importSelectionView.setItemChecked(i, selection.get(i)); + } + } + + //TODO: listview header: "Please select the settings you wish to import" + //TODO: listview footer: "Select all" / "Select none" buttons? + //TODO: listview footer: "Overwrite existing accounts?" checkbox + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.settings_import_selection)); + builder.setView(importSelectionView); + builder.setInverseBackgroundForced(true); + builder.setPositiveButton(R.string.okay_action, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ListAdapter adapter = importSelectionView.getAdapter(); + int count = adapter.getCount(); + SparseBooleanArray pos = importSelectionView.getCheckedItemPositions(); + + boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false; + List accountUuids = new ArrayList(); + int start = mImportContents.globalSettings ? 1 : 0; + for (int i = start; i < count; i++) { + if (pos.get(i)) { + accountUuids.add(mImportContents.accounts.get(i-start).uuid); + } + } + + /* + * TODO: Think some more about this. Overwriting could change the store + * type. This requires some additional code in order to work smoothly + * while the app is running. + */ + boolean overwrite = false; + + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + + ImportAsyncTask importAsyncTask = new ImportAsyncTask(activity, + includeGlobals, accountUuids, overwrite, mUri); + activity.setNonConfigurationInstance(importAsyncTask); + importAsyncTask.execute(); + } + }); + builder.setNegativeButton(R.string.cancel_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + activity.setNonConfigurationInstance(null); + } + }); + mDialog = builder.show(); + } + } + + /** + * Set the {@code NonConfigurationInstance} this activity should retain on configuration + * changes. + * + * @param inst + * The {@link NonConfigurationInstance} that should be retained when + * {@link Accounts#onRetainNonConfigurationInstance()} is called. + */ + private void setNonConfigurationInstance(NonConfigurationInstance inst) { + mNonConfigurationInstance = inst; + } + class AccountsAdapter extends ArrayAdapter { public AccountsAdapter(BaseAccount[] accounts) { super(Accounts.this, 0, accounts); @@ -1026,4 +1741,246 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } + public void onExport(final boolean includeGlobals, final Account account) { + + // TODO, prompt to allow a user to choose which accounts to export + Set accountUuids = null; + if (account != null) { + accountUuids = new HashSet(); + accountUuids.add(account.getUuid()); + } + + ExportAsyncTask asyncTask = new ExportAsyncTask(this, includeGlobals, accountUuids); + setNonConfigurationInstance(asyncTask); + asyncTask.execute(); + } + + /** + * Handles exporting of global settings and/or accounts in a background thread. + */ + private static class ExportAsyncTask extends ExtendedAsyncTask { + private boolean mIncludeGlobals; + private Set mAccountUuids; + private String mFileName; + + + private ExportAsyncTask(Accounts activity, boolean includeGlobals, + Set accountUuids) { + super(activity); + mIncludeGlobals = includeGlobals; + mAccountUuids = accountUuids; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_export_dialog_title); + String message = mContext.getString(R.string.settings_exporting); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + mFileName = SettingsExporter.exportToFile(mContext, mIncludeGlobals, + mAccountUuids); + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + if (success) { + activity.showSimpleDialog(R.string.settings_export_success_header, + R.string.settings_export_success, mFileName); + } else { + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_export_failed_header, + R.string.settings_export_failure); + } + } + } + + /** + * Handles importing of global settings and/or accounts in a background thread. + */ + private static class ImportAsyncTask extends ExtendedAsyncTask { + private boolean mIncludeGlobals; + private List mAccountUuids; + private boolean mOverwrite; + private Uri mUri; + private ImportResults mImportResults; + + private ImportAsyncTask(Accounts activity, boolean includeGlobals, + List accountUuids, boolean overwrite, Uri uri) { + super(activity); + mIncludeGlobals = includeGlobals; + mAccountUuids = accountUuids; + mOverwrite = overwrite; + mUri = uri; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_import_dialog_title); + String message = mContext.getString(R.string.settings_importing); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + InputStream is = mContext.getContentResolver().openInputStream(mUri); + try { + mImportResults = SettingsImporter.importSettings(mContext, is, + mIncludeGlobals, mAccountUuids, mOverwrite); + } finally { + try { + is.close(); + } catch (IOException e) { /* Ignore */ } + } + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during import", e); + return false; + } catch (FileNotFoundException e) { + Log.w(K9.LOG_TAG, "Couldn't open import file", e); + return false; + } catch (Exception e) { + Log.w(K9.LOG_TAG, "Unknown error", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + String filename = mUri.getLastPathSegment(); + boolean globalSettings = mImportResults.globalSettings; + int imported = mImportResults.importedAccounts.size(); + if (success && (globalSettings || imported > 0)) { + if (imported == 0) { + activity.showSimpleDialog(R.string.settings_import_success_header, + R.string.settings_import_global_settings_success, filename); + } else { + activity.showAccountsImportedDialog(mImportResults, filename); + } + + activity.refresh(); + } else { + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_import_failed_header, + R.string.settings_import_failure, filename); + } + } + } + + private static class ListImportContentsAsyncTask extends ExtendedAsyncTask { + private Uri mUri; + private ImportContents mImportContents; + + private ListImportContentsAsyncTask(Accounts activity, Uri uri) { + super(activity); + + mUri = uri; + } + + @Override + protected void showProgressDialog() { + String title = mContext.getString(R.string.settings_import_dialog_title); + String message = mContext.getString(R.string.settings_import_scanning_file); + mProgressDialog = ProgressDialog.show(mActivity, title, message, true); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + ContentResolver resolver = mContext.getContentResolver(); + InputStream is = resolver.openInputStream(mUri); + try { + mImportContents = SettingsImporter.getImportStreamContents(is); + } finally { + try { + is.close(); + } catch (IOException e) { /* Ignore */ } + } + } catch (SettingsImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + catch (FileNotFoundException e) { + Log.w(K9.LOG_TAG, "Couldn't read content from URI " + mUri); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + removeProgressDialog(); + + if (success) { + activity.showImportSelectionDialog(mImportContents, mUri); + } else { + String filename = mUri.getLastPathSegment(); + //TODO: better error messages + activity.showSimpleDialog(R.string.settings_import_failed_header, + R.string.settings_import_failure, filename); + } + } + } + + private static class MoveAccountAsyncTask extends ExtendedAsyncTask { + private Account mAccount; + private boolean mUp; + + protected MoveAccountAsyncTask(Activity activity, Account account, boolean up) { + super(activity); + mAccount = account; + mUp = up; + } + + @Override + protected void showProgressDialog() { + String message = mActivity.getString(R.string.manage_accounts_moving_message); + mProgressDialog = ProgressDialog.show(mActivity, null, message, true); + } + + @Override + protected Void doInBackground(Void... args) { + mAccount.move(Preferences.getPreferences(mContext), mUp); + return null; + } + + @Override + protected void onPostExecute(Void arg) { + Accounts activity = (Accounts) mActivity; + + // Let the activity know that the background task is complete + activity.setNonConfigurationInstance(null); + + activity.refresh(); + removeProgressDialog(); + } + } } diff --git a/src/com/fsck/k9/activity/AsyncUIProcessor.java b/src/com/fsck/k9/activity/AsyncUIProcessor.java deleted file mode 100644 index 55947b2c4..000000000 --- a/src/com/fsck/k9/activity/AsyncUIProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.fsck.k9.activity; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import android.app.Application; - -/** - * The class should be used to run long-running processes invoked from the UI that - * do not affect the Stores. There are probably pieces of MessagingController - * that can be moved here. There is no wakelock used here. Any network activity, or - * true background activity, that is invoked from here should wakelock itself. UI-centric - * activity does not need to be wakelocked, as it will simply continue when the phone wakes - * without disruption. - * - */ -public class AsyncUIProcessor { - - private final ExecutorService threadPool = Executors.newCachedThreadPool(); - private static AsyncUIProcessor inst = null; - private AsyncUIProcessor() { - } - - public synchronized static AsyncUIProcessor getInstance(Application application) { - if (inst == null) { - inst = new AsyncUIProcessor(); - } - return inst; - } - public void execute(Runnable runnable) { - threadPool.execute(runnable); - } -} diff --git a/src/com/fsck/k9/activity/ChooseAccount.java b/src/com/fsck/k9/activity/ChooseAccount.java index 6b04f6b76..0c1c0041c 100644 --- a/src/com/fsck/k9/activity/ChooseAccount.java +++ b/src/com/fsck/k9/activity/ChooseAccount.java @@ -28,6 +28,7 @@ import java.util.List; * @see K9ExpandableListActivity */ public class ChooseAccount extends K9ExpandableListActivity { + private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[0]; /** * {@link Intent} extended data name for storing {@link Account#getUuid() @@ -50,7 +51,7 @@ public class ChooseAccount extends K9ExpandableListActivity { final ExpandableListView expandableListView = getExpandableListView(); expandableListView.setItemsCanFocus(false); - final ExpandableListAdapter adapter = createAdapter(); + final IdentitiesAdapter adapter = createAdapter(); setListAdapter(adapter); expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @@ -77,7 +78,7 @@ public class ChooseAccount extends K9ExpandableListActivity { final Bundle extras = getIntent().getExtras(); final String uuid = extras.getString(EXTRA_ACCOUNT); if (uuid != null) { - final Account[] accounts = Preferences.getPreferences(this).getAccounts(); + final Account[] accounts = adapter.getAccounts(); final int length = accounts.length; for (int i = 0; i < length; i++) { final Account account = accounts[i]; @@ -106,7 +107,7 @@ public class ChooseAccount extends K9ExpandableListActivity { } } - private ExpandableListAdapter createAdapter() { + private IdentitiesAdapter createAdapter() { return new IdentitiesAdapter(this, getLayoutInflater()); } @@ -123,10 +124,13 @@ public class ChooseAccount extends K9ExpandableListActivity { private Context mContext; private LayoutInflater mLayoutInflater; + private Account[] mAccounts; public IdentitiesAdapter(final Context context, final LayoutInflater layoutInflater) { mContext = context; mLayoutInflater = layoutInflater; + Preferences prefs = Preferences.getPreferences(mContext); + mAccounts = prefs.getAvailableAccounts().toArray(EMPTY_ACCOUNT_ARRAY); } @Override @@ -233,7 +237,7 @@ public class ChooseAccount extends K9ExpandableListActivity { } private Account[] getAccounts() { - return Preferences.getPreferences(mContext).getAccounts(); + return mAccounts; } } } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index c161e23e5..47b346fcc 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -152,6 +152,7 @@ public class FolderList extends K9ListActivity { } } + /** * This class is responsible for reloading the list of local messages for a * given folder, notifying the adapter that the message have been loaded and diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java index b2c1827c6..263324671 100644 --- a/src/com/fsck/k9/activity/K9Activity.java +++ b/src/com/fsck/k9/activity/K9Activity.java @@ -195,6 +195,4 @@ public class K9Activity extends Activity { return false; } } - - } diff --git a/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java new file mode 100644 index 000000000..f8bef5741 --- /dev/null +++ b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java @@ -0,0 +1,111 @@ +package com.fsck.k9.activity.misc; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; + +/** + * Extends {@link AsyncTask} with methods to attach and detach an {@link Activity}. + * + *

+ * This is necessary to properly handle configuration changes that will restart an activity. + *

+ * Note: + * Implementing classes need to make sure they have no reference to the {@code Activity} instance + * that created the instance of that class. So if it's implemented as inner class, it needs to be + * {@code static}. + *

+ * + * @param + * see {@link AsyncTask} + * @param + * see {@link AsyncTask} + * @param + * see {@link AsyncTask} + * + * @see #restore(Activity) + * @see #retain() + */ +public abstract class ExtendedAsyncTask + extends AsyncTask implements NonConfigurationInstance { + protected Activity mActivity; + protected Context mContext; + protected ProgressDialog mProgressDialog; + + protected ExtendedAsyncTask(Activity activity) { + mActivity = activity; + mContext = activity.getApplicationContext(); + } + + /** + * Connect this {@link AsyncTask} to a new {@link Activity} instance after the activity + * was restarted due to a configuration change. + * + *

+ * This also creates a new progress dialog that is bound to the new activity. + *

+ * + * @param activity + * The new {@code Activity} instance. Never {@code null}. + */ + @Override + public void restore(Activity activity) { + mActivity = activity; + showProgressDialog(); + } + + /** + * Detach this {@link AsyncTask} from the {@link Activity} it was bound to. + * + *

+ * This needs to be called when the current activity is being destroyed during an activity + * restart due to a configuration change.
+ * We also have to destroy the progress dialog because it's bound to the activity that's + * being destroyed. + *

+ * + * @return {@code true} if this instance should be retained; {@code false} otherwise. + * + * @see Activity#onRetainNonConfigurationInstance() + */ + @Override + public boolean retain() { + boolean retain = false; + if (mProgressDialog != null) { + removeProgressDialog(); + retain = true; + } + mActivity = null; + + return retain; + } + + /** + * Creates a {@link ProgressDialog} that is shown while the background thread is running. + * + *

+ * This needs to store a {@code ProgressDialog} instance in {@link #mProgressDialog} or + * override {@link #removeProgressDialog()}. + *

+ */ + protected abstract void showProgressDialog(); + + protected void removeProgressDialog() { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + + /** + * This default implementation only creates a progress dialog. + * + *

+ * Important: + * Be sure to call {@link #removeProgressDialog()} in {@link AsyncTask#onPostExecute(Object)}. + *

+ */ + @Override + protected void onPreExecute() { + showProgressDialog(); + } +} diff --git a/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java b/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java new file mode 100644 index 000000000..d9f314581 --- /dev/null +++ b/src/com/fsck/k9/activity/misc/NonConfigurationInstance.java @@ -0,0 +1,38 @@ +package com.fsck.k9.activity.misc; + +import android.app.Activity; + + +public interface NonConfigurationInstance { + /** + * Decide whether to retain this {@code NonConfigurationInstance} and clean up resources if + * necessary. + * + *

+ * This needs to be called when the current activity is being destroyed during an activity + * restart due to a configuration change.
+ * Implementations should make sure that references to the {@code Activity} instance that is + * about to be destroyed are cleared to avoid memory leaks. This includes all UI elements that + * are bound to an activity (e.g. dialogs). They can be re-created in + * {@link #restore(Activity)}. + *

+ * + * @return {@code true} if this instance should be retained; {@code false} otherwise. + * + * @see Activity#onRetainNonConfigurationInstance() + */ + public boolean retain(); + + /** + * Connect this retained {@code NonConfigurationInstance} to the new {@link Activity} instance + * after the activity was restarted due to a configuration change. + * + *

+ * This also creates a new progress dialog that is bound to the new activity. + *

+ * + * @param activity + * The new {@code Activity} instance. Never {@code null}. + */ + public void restore(Activity activity); +} diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 49662d576..2eedc119e 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -650,15 +650,10 @@ public class MessagingController implements Runnable { accountUuidsSet.addAll(Arrays.asList(accountUuids)); } final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext()); - Account[] accounts = prefs.getAccounts(); List foldersToSearch = null; boolean displayableOnly = false; boolean noSpecialFolders = true; - for (final Account account : accounts) { - if (!account.isAvailable(mApplication)) { - Log.d(K9.LOG_TAG, "searchLocalMessagesSynchronous() ignores account that is not available"); - continue; - } + for (final Account account : prefs.getAvailableAccounts()) { if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) { continue; } @@ -2837,8 +2832,7 @@ public class MessagingController implements Runnable { public void sendPendingMessages(MessagingListener listener) { final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext()); - Account[] accounts = prefs.getAccounts(); - for (Account account : accounts) { + for (Account account : prefs.getAvailableAccounts()) { sendPendingMessages(account, listener); } } @@ -3598,13 +3592,12 @@ public class MessagingController implements Runnable { Log.i(K9.LOG_TAG, "Starting mail check"); Preferences prefs = Preferences.getPreferences(context); - Account[] accounts; + Collection accounts; if (account != null) { - accounts = new Account[] { - account - }; + accounts = new ArrayList(1); + accounts.add(account); } else { - accounts = prefs.getAccounts(); + accounts = prefs.getAvailableAccounts(); } for (final Account account : accounts) { diff --git a/src/com/fsck/k9/helper/DateFormatter.java b/src/com/fsck/k9/helper/DateFormatter.java index 12a9e1a31..784195f37 100644 --- a/src/com/fsck/k9/helper/DateFormatter.java +++ b/src/com/fsck/k9/helper/DateFormatter.java @@ -43,6 +43,10 @@ public class DateFormatter { } }; + public static void clearChosenFormat() { + sChosenFormat = null; + } + public static DateFormat getDateFormat(Context context, String formatString) { java.text.DateFormat dateFormat; diff --git a/src/com/fsck/k9/mail/ConnectionSecurity.java b/src/com/fsck/k9/mail/ConnectionSecurity.java new file mode 100644 index 000000000..98741303e --- /dev/null +++ b/src/com/fsck/k9/mail/ConnectionSecurity.java @@ -0,0 +1,19 @@ +package com.fsck.k9.mail; + +/** + * The currently available connection security types. + * + *

+ * Right now this enum is only used by {@link ServerSettings} and converted to store- or + * transport-specific constants in the different {@link Store} and {@link Transport} + * implementations. In the future we probably want to change this and use + * {@code ConnectionSecurity} exclusively. + *

+ */ +public enum ConnectionSecurity { + NONE, + STARTTLS_OPTIONAL, + STARTTLS_REQUIRED, + SSL_TLS_OPTIONAL, + SSL_TLS_REQUIRED +} diff --git a/src/com/fsck/k9/mail/ServerSettings.java b/src/com/fsck/k9/mail/ServerSettings.java new file mode 100644 index 000000000..f7f127d29 --- /dev/null +++ b/src/com/fsck/k9/mail/ServerSettings.java @@ -0,0 +1,133 @@ +package com.fsck.k9.mail; + +import java.util.Map; +import com.fsck.k9.Account; + +/** + * This is an abstraction to get rid of the store- and transport-specific URIs. + * + *

+ * Right now it's only used for settings import/export. But the goal is to get rid of + * store/transport URIs altogether. + *

+ * + * @see Account#getStoreUri() + * @see Account#getTransportUri() + */ +public class ServerSettings { + /** + * Name of the store or transport type (e.g. "IMAP"). + */ + public final String type; + + /** + * The host name of the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String host; + + /** + * The port number of the server. + * + * {@code -1} if not applicable for the store or transport. + */ + public final int port; + + /** + * The type of connection security to be used when connecting to the server. + * + * {@link ConnectionSecurity#NONE} if not applicable for the store or transport. + */ + public final ConnectionSecurity connectionSecurity; + + /** + * The authentication method to use when connecting to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String authenticationType; + + /** + * The username part of the credentials needed to authenticate to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String username; + + /** + * The password part of the credentials needed to authenticate to the server. + * + * {@code null} if not applicable for the store or transport. + */ + public final String password; + + + /** + * Creates a new {@code ServerSettings} object. + * + * @param type + * see {@link ServerSettings#type} + * @param host + * see {@link ServerSettings#host} + * @param port + * see {@link ServerSettings#port} + * @param connectionSecurity + * see {@link ServerSettings#connectionSecurity} + * @param authenticationType + * see {@link ServerSettings#authenticationType} + * @param username + * see {@link ServerSettings#username} + * @param password + * see {@link ServerSettings#password} + */ + public ServerSettings(String type, String host, int port, + ConnectionSecurity connectionSecurity, String authenticationType, String username, + String password) { + this.type = type; + this.host = host; + this.port = port; + this.connectionSecurity = connectionSecurity; + this.authenticationType = authenticationType; + this.username = username; + this.password = password; + } + + /** + * Creates an "empty" {@code ServerSettings} object. + * + * Everything but {@link ServerSettings#type} is unused. + * + * @param type + * see {@link ServerSettings#type} + */ + public ServerSettings(String type) { + this.type = type; + host = null; + port = -1; + connectionSecurity = ConnectionSecurity.NONE; + authenticationType = null; + username = null; + password = null; + } + + /** + * Returns store- or transport-specific settings as key/value pair. + * + *

Classes that inherit from this one are expected to override this method.

+ */ + public Map getExtra() { + return null; + } + + protected void putIfNotNull(Map map, String key, String value) { + if (value != null) { + map.put(key, value); + } + } + + public ServerSettings newPassword(String newPassword) { + return new ServerSettings(type, host, port, connectionSecurity, authenticationType, + username, newPassword); + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 904a16baa..5013a8f29 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -29,17 +29,13 @@ public abstract class Store { /** * Remote stores indexed by Uri. */ - private static HashMap mStores = new HashMap(); + private static HashMap sStores = new HashMap(); + /** * Local stores indexed by UUid because the Uri may change due to migration to/from SD-card. */ - private static HashMap mLocalStores = new HashMap(); + private static HashMap sLocalStores = new HashMap(); - protected final Account mAccount; - - protected Store(Account account) { - mAccount = account; - } /** * Get an instance of a remote mail store. @@ -51,7 +47,7 @@ public abstract class Store { throw new RuntimeException("Asked to get non-local Store object but given LocalStore URI"); } - Store store = mStores.get(uri); + Store store = sStores.get(uri); if (store == null) { if (uri.startsWith("imap")) { store = new ImapStore(account); @@ -62,7 +58,7 @@ public abstract class Store { } if (store != null) { - mStores.put(uri, store); + sStores.put(uri, store); } } @@ -78,15 +74,72 @@ public abstract class Store { * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} */ public synchronized static LocalStore getLocalInstance(Account account, Application application) throws MessagingException { - Store store = mLocalStores.get(account.getUuid()); + Store store = sLocalStores.get(account.getUuid()); if (store == null) { store = new LocalStore(account, application); - mLocalStores.put(account.getUuid(), store); + sLocalStores.put(account.getUuid(), store); } return (LocalStore) store; } + /** + * 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; @@ -96,20 +149,23 @@ public abstract class Store { public boolean isCopyCapable() { return false; } + public boolean isMoveCapable() { return false; } + public boolean isPushCapable() { return false; } + public boolean isSendCapable() { return false; } + public boolean isExpungeCapable() { return false; } - public void sendMessages(Message[] messages) throws MessagingException { } diff --git a/src/com/fsck/k9/mail/Transport.java b/src/com/fsck/k9/mail/Transport.java index 04634f01e..a0450ffeb 100644 --- a/src/com/fsck/k9/mail/Transport.java +++ b/src/com/fsck/k9/mail/Transport.java @@ -22,6 +22,50 @@ public abstract class Transport { } } + /** + * Decodes the contents of transport-specific URIs and puts them into a {@link ServerSettings} + * object. + * + * @param uri + * the transport-specific URI to decode + * + * @return A {@link ServerSettings} object holding the settings contained in the URI. + * + * @see SmtpTransport#decodeUri(String) + * @see WebDavTransport#decodeUri(String) + */ + public static ServerSettings decodeTransportUri(String uri) { + if (uri.startsWith("smtp")) { + return SmtpTransport.decodeUri(uri); + } else if (uri.startsWith("webdav")) { + return WebDavTransport.decodeUri(uri); + } else { + throw new IllegalArgumentException("Not a valid transport URI"); + } + } + + /** + * Creates a transport URI from the information supplied in the {@link ServerSettings} object. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A transport URI that holds the same information as the {@code server} parameter. + * + * @see SmtpTransport#createUri(ServerSettings) + * @see WebDavTransport#createUri(ServerSettings) + */ + public static String createTransportUri(ServerSettings server) { + if (SmtpTransport.TRANSPORT_TYPE.equals(server.type)) { + return SmtpTransport.createUri(server); + } else if (WebDavTransport.TRANSPORT_TYPE.equals(server.type)) { + return WebDavTransport.createUri(server); + } else { + throw new IllegalArgumentException("Not a valid transport URI"); + } + } + + public abstract void open() throws MessagingException; public abstract void sendMessage(Message message) throws MessagingException; diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 2922a8bd7..5d6614d8f 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -21,7 +21,7 @@ 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"; /* * http://www.w3schools.com/media/media_mimeref.asp @@ -29,853 +29,856 @@ public class MimeUtility { * http://www.stdicon.com/mimetypes */ public static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { - { "", "application/octet-stream" }, - { "123", "application/vnd.lotus-1-2-3"}, - { "323", "text/h323"}, - { "3dml", "text/vnd.in3d.3dml"}, - { "3g2", "video/3gpp2"}, - { "3gp", "video/3gpp"}, - { "aab", "application/x-authorware-bin"}, - { "aac", "audio/x-aac"}, - { "aam", "application/x-authorware-map"}, - { "a", "application/octet-stream"}, - { "aas", "application/x-authorware-seg"}, - { "abw", "application/x-abiword"}, - { "acc", "application/vnd.americandynamics.acc"}, - { "ace", "application/x-ace-compressed"}, - { "acu", "application/vnd.acucobol"}, - { "acutc", "application/vnd.acucorp"}, - { "acx", "application/internet-property-stream"}, - { "adp", "audio/adpcm"}, - { "aep", "application/vnd.audiograph"}, - { "afm", "application/x-font-type1"}, - { "afp", "application/vnd.ibm.modcap"}, - { "ai", "application/postscript"}, - { "aif", "audio/x-aiff"}, - { "aifc", "audio/x-aiff"}, - { "aiff", "audio/x-aiff"}, - { "air", "application/vnd.adobe.air-application-installer-package+zip"}, - { "ami", "application/vnd.amiga.ami"}, - { "apk", "application/vnd.android.package-archive"}, - { "application", "application/x-ms-application"}, - { "apr", "application/vnd.lotus-approach"}, - { "asc", "application/pgp-signature"}, - { "asf", "video/x-ms-asf"}, - { "asm", "text/x-asm"}, - { "aso", "application/vnd.accpac.simply.aso"}, - { "asr", "video/x-ms-asf"}, - { "asx", "video/x-ms-asf"}, - { "atc", "application/vnd.acucorp"}, - { "atom", "application/atom+xml"}, - { "atomcat", "application/atomcat+xml"}, - { "atomsvc", "application/atomsvc+xml"}, - { "atx", "application/vnd.antix.game-component"}, - { "au", "audio/basic"}, - { "avi", "video/x-msvideo"}, - { "aw", "application/applixware"}, - { "axs", "application/olescript"}, - { "azf", "application/vnd.airzip.filesecure.azf"}, - { "azs", "application/vnd.airzip.filesecure.azs"}, - { "azw", "application/vnd.amazon.ebook"}, - { "bas", "text/plain"}, - { "bat", "application/x-msdownload"}, - { "bcpio", "application/x-bcpio"}, - { "bdf", "application/x-font-bdf"}, - { "bdm", "application/vnd.syncml.dm+wbxml"}, - { "bh2", "application/vnd.fujitsu.oasysprs"}, - { "bin", "application/octet-stream"}, - { "bmi", "application/vnd.bmi"}, - { "bmp", "image/bmp"}, - { "book", "application/vnd.framemaker"}, - { "box", "application/vnd.previewsystems.box"}, - { "boz", "application/x-bzip2"}, - { "bpk", "application/octet-stream"}, - { "btif", "image/prs.btif"}, - { "bz2", "application/x-bzip2"}, - { "bz", "application/x-bzip"}, - { "c4d", "application/vnd.clonk.c4group"}, - { "c4f", "application/vnd.clonk.c4group"}, - { "c4g", "application/vnd.clonk.c4group"}, - { "c4p", "application/vnd.clonk.c4group"}, - { "c4u", "application/vnd.clonk.c4group"}, - { "cab", "application/vnd.ms-cab-compressed"}, - { "car", "application/vnd.curl.car"}, - { "cat", "application/vnd.ms-pki.seccat"}, - { "cct", "application/x-director"}, - { "cc", "text/x-c"}, - { "ccxml", "application/ccxml+xml"}, - { "cdbcmsg", "application/vnd.contact.cmsg"}, - { "cdf", "application/x-cdf"}, - { "cdkey", "application/vnd.mediastation.cdkey"}, - { "cdx", "chemical/x-cdx"}, - { "cdxml", "application/vnd.chemdraw+xml"}, - { "cdy", "application/vnd.cinderella"}, - { "cer", "application/x-x509-ca-cert"}, - { "cgm", "image/cgm"}, - { "chat", "application/x-chat"}, - { "chm", "application/vnd.ms-htmlhelp"}, - { "chrt", "application/vnd.kde.kchart"}, - { "cif", "chemical/x-cif"}, - { "cii", "application/vnd.anser-web-certificate-issue-initiation"}, - { "cla", "application/vnd.claymore"}, - { "class", "application/java-vm"}, - { "clkk", "application/vnd.crick.clicker.keyboard"}, - { "clkp", "application/vnd.crick.clicker.palette"}, - { "clkt", "application/vnd.crick.clicker.template"}, - { "clkw", "application/vnd.crick.clicker.wordbank"}, - { "clkx", "application/vnd.crick.clicker"}, - { "clp", "application/x-msclip"}, - { "cmc", "application/vnd.cosmocaller"}, - { "cmdf", "chemical/x-cmdf"}, - { "cml", "chemical/x-cml"}, - { "cmp", "application/vnd.yellowriver-custom-menu"}, - { "cmx", "image/x-cmx"}, - { "cod", "application/vnd.rim.cod"}, - { "com", "application/x-msdownload"}, - { "conf", "text/plain"}, - { "cpio", "application/x-cpio"}, - { "cpp", "text/x-c"}, - { "cpt", "application/mac-compactpro"}, - { "crd", "application/x-mscardfile"}, - { "crl", "application/pkix-crl"}, - { "crt", "application/x-x509-ca-cert"}, - { "csh", "application/x-csh"}, - { "csml", "chemical/x-csml"}, - { "csp", "application/vnd.commonspace"}, - { "css", "text/css"}, - { "cst", "application/x-director"}, - { "csv", "text/csv"}, - { "c", "text/plain"}, - { "cu", "application/cu-seeme"}, - { "curl", "text/vnd.curl"}, - { "cww", "application/prs.cww"}, - { "cxt", "application/x-director"}, - { "cxx", "text/x-c"}, - { "daf", "application/vnd.mobius.daf"}, - { "dataless", "application/vnd.fdsn.seed"}, - { "davmount", "application/davmount+xml"}, - { "dcr", "application/x-director"}, - { "dcurl", "text/vnd.curl.dcurl"}, - { "dd2", "application/vnd.oma.dd2+xml"}, - { "ddd", "application/vnd.fujixerox.ddd"}, - { "deb", "application/x-debian-package"}, - { "def", "text/plain"}, - { "deploy", "application/octet-stream"}, - { "der", "application/x-x509-ca-cert"}, - { "dfac", "application/vnd.dreamfactory"}, - { "dic", "text/x-c"}, - { "diff", "text/plain"}, - { "dir", "application/x-director"}, - { "dis", "application/vnd.mobius.dis"}, - { "dist", "application/octet-stream"}, - { "distz", "application/octet-stream"}, - { "djv", "image/vnd.djvu"}, - { "djvu", "image/vnd.djvu"}, - { "dll", "application/x-msdownload"}, - { "dmg", "application/octet-stream"}, - { "dms", "application/octet-stream"}, - { "dna", "application/vnd.dna"}, - { "doc", "application/msword"}, - { "docm", "application/vnd.ms-word.document.macroenabled.12"}, - { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - { "dot", "application/msword"}, - { "dotm", "application/vnd.ms-word.template.macroenabled.12"}, - { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, - { "dp", "application/vnd.osgi.dp"}, - { "dpg", "application/vnd.dpgraph"}, - { "dsc", "text/prs.lines.tag"}, - { "dtb", "application/x-dtbook+xml"}, - { "dtd", "application/xml-dtd"}, - { "dts", "audio/vnd.dts"}, - { "dtshd", "audio/vnd.dts.hd"}, - { "dump", "application/octet-stream"}, - { "dvi", "application/x-dvi"}, - { "dwf", "model/vnd.dwf"}, - { "dwg", "image/vnd.dwg"}, - { "dxf", "image/vnd.dxf"}, - { "dxp", "application/vnd.spotfire.dxp"}, - { "dxr", "application/x-director"}, - { "ecelp4800", "audio/vnd.nuera.ecelp4800"}, - { "ecelp7470", "audio/vnd.nuera.ecelp7470"}, - { "ecelp9600", "audio/vnd.nuera.ecelp9600"}, - { "ecma", "application/ecmascript"}, - { "edm", "application/vnd.novadigm.edm"}, - { "edx", "application/vnd.novadigm.edx"}, - { "efif", "application/vnd.picsel"}, - { "ei6", "application/vnd.pg.osasli"}, - { "elc", "application/octet-stream"}, - { "eml", "message/rfc822"}, - { "emma", "application/emma+xml"}, - { "eol", "audio/vnd.digital-winds"}, - { "eot", "application/vnd.ms-fontobject"}, - { "eps", "application/postscript"}, - { "epub", "application/epub+zip"}, - { "es3", "application/vnd.eszigno3+xml"}, - { "esf", "application/vnd.epson.esf"}, - { "et3", "application/vnd.eszigno3+xml"}, - { "etx", "text/x-setext"}, - { "evy", "application/envoy"}, - { "exe", "application/octet-stream"}, - { "ext", "application/vnd.novadigm.ext"}, - { "ez2", "application/vnd.ezpix-album"}, - { "ez3", "application/vnd.ezpix-package"}, - { "ez", "application/andrew-inset"}, - { "f4v", "video/x-f4v"}, - { "f77", "text/x-fortran"}, - { "f90", "text/x-fortran"}, - { "fbs", "image/vnd.fastbidsheet"}, - { "fdf", "application/vnd.fdf"}, - { "fe_launch", "application/vnd.denovo.fcselayout-link"}, - { "fg5", "application/vnd.fujitsu.oasysgp"}, - { "fgd", "application/x-director"}, - { "fh4", "image/x-freehand"}, - { "fh5", "image/x-freehand"}, - { "fh7", "image/x-freehand"}, - { "fhc", "image/x-freehand"}, - { "fh", "image/x-freehand"}, - { "fif", "application/fractals"}, - { "fig", "application/x-xfig"}, - { "fli", "video/x-fli"}, - { "flo", "application/vnd.micrografx.flo"}, - { "flr", "x-world/x-vrml"}, - { "flv", "video/x-flv"}, - { "flw", "application/vnd.kde.kivio"}, - { "flx", "text/vnd.fmi.flexstor"}, - { "fly", "text/vnd.fly"}, - { "fm", "application/vnd.framemaker"}, - { "fnc", "application/vnd.frogans.fnc"}, - { "for", "text/x-fortran"}, - { "fpx", "image/vnd.fpx"}, - { "frame", "application/vnd.framemaker"}, - { "fsc", "application/vnd.fsc.weblaunch"}, - { "fst", "image/vnd.fst"}, - { "ftc", "application/vnd.fluxtime.clip"}, - { "f", "text/x-fortran"}, - { "fti", "application/vnd.anser-web-funds-transfer-initiation"}, - { "fvt", "video/vnd.fvt"}, - { "fzs", "application/vnd.fuzzysheet"}, - { "g3", "image/g3fax"}, - { "gac", "application/vnd.groove-account"}, - { "gdl", "model/vnd.gdl"}, - { "geo", "application/vnd.dynageo"}, - { "gex", "application/vnd.geometry-explorer"}, - { "ggb", "application/vnd.geogebra.file"}, - { "ggt", "application/vnd.geogebra.tool"}, - { "ghf", "application/vnd.groove-help"}, - { "gif", "image/gif"}, - { "gim", "application/vnd.groove-identity-message"}, - { "gmx", "application/vnd.gmx"}, - { "gnumeric", "application/x-gnumeric"}, - { "gph", "application/vnd.flographit"}, - { "gqf", "application/vnd.grafeq"}, - { "gqs", "application/vnd.grafeq"}, - { "gram", "application/srgs"}, - { "gre", "application/vnd.geometry-explorer"}, - { "grv", "application/vnd.groove-injector"}, - { "grxml", "application/srgs+xml"}, - { "gsf", "application/x-font-ghostscript"}, - { "gtar", "application/x-gtar"}, - { "gtm", "application/vnd.groove-tool-message"}, - { "gtw", "model/vnd.gtw"}, - { "gv", "text/vnd.graphviz"}, - { "gz", "application/x-gzip"}, - { "h261", "video/h261"}, - { "h263", "video/h263"}, - { "h264", "video/h264"}, - { "hbci", "application/vnd.hbci"}, - { "hdf", "application/x-hdf"}, - { "hh", "text/x-c"}, - { "hlp", "application/winhlp"}, - { "hpgl", "application/vnd.hp-hpgl"}, - { "hpid", "application/vnd.hp-hpid"}, - { "hps", "application/vnd.hp-hps"}, - { "hqx", "application/mac-binhex40"}, - { "hta", "application/hta"}, - { "htc", "text/x-component"}, - { "h", "text/plain"}, - { "htke", "application/vnd.kenameaapp"}, - { "html", "text/html"}, - { "htm", "text/html"}, - { "htt", "text/webviewhtml"}, - { "hvd", "application/vnd.yamaha.hv-dic"}, - { "hvp", "application/vnd.yamaha.hv-voice"}, - { "hvs", "application/vnd.yamaha.hv-script"}, - { "icc", "application/vnd.iccprofile"}, - { "ice", "x-conference/x-cooltalk"}, - { "icm", "application/vnd.iccprofile"}, - { "ico", "image/x-icon"}, - { "ics", "text/calendar"}, - { "ief", "image/ief"}, - { "ifb", "text/calendar"}, - { "ifm", "application/vnd.shana.informed.formdata"}, - { "iges", "model/iges"}, - { "igl", "application/vnd.igloader"}, - { "igs", "model/iges"}, - { "igx", "application/vnd.micrografx.igx"}, - { "iif", "application/vnd.shana.informed.interchange"}, - { "iii", "application/x-iphone"}, - { "imp", "application/vnd.accpac.simply.imp"}, - { "ims", "application/vnd.ms-ims"}, - { "ins", "application/x-internet-signup"}, - { "in", "text/plain"}, - { "ipk", "application/vnd.shana.informed.package"}, - { "irm", "application/vnd.ibm.rights-management"}, - { "irp", "application/vnd.irepository.package+xml"}, - { "iso", "application/octet-stream"}, - { "isp", "application/x-internet-signup"}, - { "itp", "application/vnd.shana.informed.formtemplate"}, - { "ivp", "application/vnd.immervision-ivp"}, - { "ivu", "application/vnd.immervision-ivu"}, - { "jad", "text/vnd.sun.j2me.app-descriptor"}, - { "jam", "application/vnd.jam"}, - { "jar", "application/java-archive"}, - { "java", "text/x-java-source"}, - { "jfif", "image/pipeg"}, - { "jisp", "application/vnd.jisp"}, - { "jlt", "application/vnd.hp-jlyt"}, - { "jnlp", "application/x-java-jnlp-file"}, - { "joda", "application/vnd.joost.joda-archive"}, - { "jpeg", "image/jpeg"}, - { "jpe", "image/jpeg"}, - { "jpg", "image/jpeg"}, - { "jpgm", "video/jpm"}, - { "jpgv", "video/jpeg"}, - { "jpm", "video/jpm"}, - { "js", "application/x-javascript"}, - { "json", "application/json"}, - { "kar", "audio/midi"}, - { "karbon", "application/vnd.kde.karbon"}, - { "kfo", "application/vnd.kde.kformula"}, - { "kia", "application/vnd.kidspiration"}, - { "kil", "application/x-killustrator"}, - { "kml", "application/vnd.google-earth.kml+xml"}, - { "kmz", "application/vnd.google-earth.kmz"}, - { "kne", "application/vnd.kinar"}, - { "knp", "application/vnd.kinar"}, - { "kon", "application/vnd.kde.kontour"}, - { "kpr", "application/vnd.kde.kpresenter"}, - { "kpt", "application/vnd.kde.kpresenter"}, - { "ksh", "text/plain"}, - { "ksp", "application/vnd.kde.kspread"}, - { "ktr", "application/vnd.kahootz"}, - { "ktz", "application/vnd.kahootz"}, - { "kwd", "application/vnd.kde.kword"}, - { "kwt", "application/vnd.kde.kword"}, - { "latex", "application/x-latex"}, - { "lbd", "application/vnd.llamagraphics.life-balance.desktop"}, - { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, - { "les", "application/vnd.hhe.lesson-player"}, - { "lha", "application/octet-stream"}, - { "link66", "application/vnd.route66.link66+xml"}, - { "list3820", "application/vnd.ibm.modcap"}, - { "listafp", "application/vnd.ibm.modcap"}, - { "list", "text/plain"}, - { "log", "text/plain"}, - { "lostxml", "application/lost+xml"}, - { "lrf", "application/octet-stream"}, - { "lrm", "application/vnd.ms-lrm"}, - { "lsf", "video/x-la-asf"}, - { "lsx", "video/x-la-asf"}, - { "ltf", "application/vnd.frogans.ltf"}, - { "lvp", "audio/vnd.lucent.voice"}, - { "lwp", "application/vnd.lotus-wordpro"}, - { "lzh", "application/octet-stream"}, - { "m13", "application/x-msmediaview"}, - { "m14", "application/x-msmediaview"}, - { "m1v", "video/mpeg"}, - { "m2a", "audio/mpeg"}, - { "m2v", "video/mpeg"}, - { "m3a", "audio/mpeg"}, - { "m3u", "audio/x-mpegurl"}, - { "m4u", "video/vnd.mpegurl"}, - { "m4v", "video/x-m4v"}, - { "ma", "application/mathematica"}, - { "mag", "application/vnd.ecowin.chart"}, - { "maker", "application/vnd.framemaker"}, - { "man", "text/troff"}, - { "mathml", "application/mathml+xml"}, - { "mb", "application/mathematica"}, - { "mbk", "application/vnd.mobius.mbk"}, - { "mbox", "application/mbox"}, - { "mc1", "application/vnd.medcalcdata"}, - { "mcd", "application/vnd.mcd"}, - { "mcurl", "text/vnd.curl.mcurl"}, - { "mdb", "application/x-msaccess"}, - { "mdi", "image/vnd.ms-modi"}, - { "mesh", "model/mesh"}, - { "me", "text/troff"}, - { "mfm", "application/vnd.mfmp"}, - { "mgz", "application/vnd.proteus.magazine"}, - { "mht", "message/rfc822"}, - { "mhtml", "message/rfc822"}, - { "mid", "audio/midi"}, - { "midi", "audio/midi"}, - { "mif", "application/vnd.mif"}, - { "mime", "message/rfc822"}, - { "mj2", "video/mj2"}, - { "mjp2", "video/mj2"}, - { "mlp", "application/vnd.dolby.mlp"}, - { "mmd", "application/vnd.chipnuts.karaoke-mmd"}, - { "mmf", "application/vnd.smaf"}, - { "mmr", "image/vnd.fujixerox.edmics-mmr"}, - { "mny", "application/x-msmoney"}, - { "mobi", "application/x-mobipocket-ebook"}, - { "movie", "video/x-sgi-movie"}, - { "mov", "video/quicktime"}, - { "mp2a", "audio/mpeg"}, - { "mp2", "video/mpeg"}, - { "mp3", "audio/mpeg"}, - { "mp4a", "audio/mp4"}, - { "mp4s", "application/mp4"}, - { "mp4", "video/mp4"}, - { "mp4v", "video/mp4"}, - { "mpa", "video/mpeg"}, - { "mpc", "application/vnd.mophun.certificate"}, - { "mpeg", "video/mpeg"}, - { "mpe", "video/mpeg"}, - { "mpg4", "video/mp4"}, - { "mpga", "audio/mpeg"}, - { "mpg", "video/mpeg"}, - { "mpkg", "application/vnd.apple.installer+xml"}, - { "mpm", "application/vnd.blueice.multipass"}, - { "mpn", "application/vnd.mophun.application"}, - { "mpp", "application/vnd.ms-project"}, - { "mpt", "application/vnd.ms-project"}, - { "mpv2", "video/mpeg"}, - { "mpy", "application/vnd.ibm.minipay"}, - { "mqy", "application/vnd.mobius.mqy"}, - { "mrc", "application/marc"}, - { "mscml", "application/mediaservercontrol+xml"}, - { "mseed", "application/vnd.fdsn.mseed"}, - { "mseq", "application/vnd.mseq"}, - { "msf", "application/vnd.epson.msf"}, - { "msh", "model/mesh"}, - { "msi", "application/x-msdownload"}, - { "ms", "text/troff"}, - { "msty", "application/vnd.muvee.style"}, - { "mts", "model/vnd.mts"}, - { "mus", "application/vnd.musician"}, - { "musicxml", "application/vnd.recordare.musicxml+xml"}, - { "mvb", "application/x-msmediaview"}, - { "mxf", "application/mxf"}, - { "mxl", "application/vnd.recordare.musicxml"}, - { "mxml", "application/xv+xml"}, - { "mxs", "application/vnd.triscape.mxs"}, - { "mxu", "video/vnd.mpegurl"}, - { "nb", "application/mathematica"}, - { "nc", "application/x-netcdf"}, - { "ncx", "application/x-dtbncx+xml"}, - { "n-gage", "application/vnd.nokia.n-gage.symbian.install"}, - { "ngdat", "application/vnd.nokia.n-gage.data"}, - { "nlu", "application/vnd.neurolanguage.nlu"}, - { "nml", "application/vnd.enliven"}, - { "nnd", "application/vnd.noblenet-directory"}, - { "nns", "application/vnd.noblenet-sealer"}, - { "nnw", "application/vnd.noblenet-web"}, - { "npx", "image/vnd.net-fpx"}, - { "nsf", "application/vnd.lotus-notes"}, - { "nws", "message/rfc822"}, - { "oa2", "application/vnd.fujitsu.oasys2"}, - { "oa3", "application/vnd.fujitsu.oasys3"}, - { "o", "application/octet-stream"}, - { "oas", "application/vnd.fujitsu.oasys"}, - { "obd", "application/x-msbinder"}, - { "obj", "application/octet-stream"}, - { "oda", "application/oda"}, - { "odb", "application/vnd.oasis.opendocument.database"}, - { "odc", "application/vnd.oasis.opendocument.chart"}, - { "odf", "application/vnd.oasis.opendocument.formula"}, - { "odft", "application/vnd.oasis.opendocument.formula-template"}, - { "odg", "application/vnd.oasis.opendocument.graphics"}, - { "odi", "application/vnd.oasis.opendocument.image"}, - { "odp", "application/vnd.oasis.opendocument.presentation"}, - { "ods", "application/vnd.oasis.opendocument.spreadsheet"}, - { "odt", "application/vnd.oasis.opendocument.text"}, - { "oga", "audio/ogg"}, - { "ogg", "audio/ogg"}, - { "ogv", "video/ogg"}, - { "ogx", "application/ogg"}, - { "onepkg", "application/onenote"}, - { "onetmp", "application/onenote"}, - { "onetoc2", "application/onenote"}, - { "onetoc", "application/onenote"}, - { "opf", "application/oebps-package+xml"}, - { "oprc", "application/vnd.palm"}, - { "org", "application/vnd.lotus-organizer"}, - { "osf", "application/vnd.yamaha.openscoreformat"}, - { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, - { "otc", "application/vnd.oasis.opendocument.chart-template"}, - { "otf", "application/x-font-otf"}, - { "otg", "application/vnd.oasis.opendocument.graphics-template"}, - { "oth", "application/vnd.oasis.opendocument.text-web"}, - { "oti", "application/vnd.oasis.opendocument.image-template"}, - { "otm", "application/vnd.oasis.opendocument.text-master"}, - { "otp", "application/vnd.oasis.opendocument.presentation-template"}, - { "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, - { "ott", "application/vnd.oasis.opendocument.text-template"}, - { "oxt", "application/vnd.openofficeorg.extension"}, - { "p10", "application/pkcs10"}, - { "p12", "application/x-pkcs12"}, - { "p7b", "application/x-pkcs7-certificates"}, - { "p7c", "application/x-pkcs7-mime"}, - { "p7m", "application/x-pkcs7-mime"}, - { "p7r", "application/x-pkcs7-certreqresp"}, - { "p7s", "application/x-pkcs7-signature"}, - { "pas", "text/x-pascal"}, - { "pbd", "application/vnd.powerbuilder6"}, - { "pbm", "image/x-portable-bitmap"}, - { "pcf", "application/x-font-pcf"}, - { "pcl", "application/vnd.hp-pcl"}, - { "pclxl", "application/vnd.hp-pclxl"}, - { "pct", "image/x-pict"}, - { "pcurl", "application/vnd.curl.pcurl"}, - { "pcx", "image/x-pcx"}, - { "pdb", "application/vnd.palm"}, - { "pdf", "application/pdf"}, - { "pfa", "application/x-font-type1"}, - { "pfb", "application/x-font-type1"}, - { "pfm", "application/x-font-type1"}, - { "pfr", "application/font-tdpfr"}, - { "pfx", "application/x-pkcs12"}, - { "pgm", "image/x-portable-graymap"}, - { "pgn", "application/x-chess-pgn"}, - { "pgp", "application/pgp-encrypted"}, - { "pic", "image/x-pict"}, - { "pkg", "application/octet-stream"}, - { "pki", "application/pkixcmp"}, - { "pkipath", "application/pkix-pkipath"}, - { "pko", "application/ynd.ms-pkipko"}, - { "plb", "application/vnd.3gpp.pic-bw-large"}, - { "plc", "application/vnd.mobius.plc"}, - { "plf", "application/vnd.pocketlearn"}, - { "pls", "application/pls+xml"}, - { "pl", "text/plain"}, - { "pma", "application/x-perfmon"}, - { "pmc", "application/x-perfmon"}, - { "pml", "application/x-perfmon"}, - { "pmr", "application/x-perfmon"}, - { "pmw", "application/x-perfmon"}, - { "png", "image/png"}, - { "pnm", "image/x-portable-anymap"}, - { "portpkg", "application/vnd.macports.portpkg"}, - { "pot,", "application/vnd.ms-powerpoint"}, - { "pot", "application/vnd.ms-powerpoint"}, - { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, - { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, - { "ppa", "application/vnd.ms-powerpoint"}, - { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, - { "ppd", "application/vnd.cups-ppd"}, - { "ppm", "image/x-portable-pixmap"}, - { "pps", "application/vnd.ms-powerpoint"}, - { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, - { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, - { "ppt", "application/vnd.ms-powerpoint"}, - { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, - { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - { "pqa", "application/vnd.palm"}, - { "prc", "application/x-mobipocket-ebook"}, - { "pre", "application/vnd.lotus-freelance"}, - { "prf", "application/pics-rules"}, - { "ps", "application/postscript"}, - { "psb", "application/vnd.3gpp.pic-bw-small"}, - { "psd", "image/vnd.adobe.photoshop"}, - { "psf", "application/x-font-linux-psf"}, - { "p", "text/x-pascal"}, - { "ptid", "application/vnd.pvi.ptid1"}, - { "pub", "application/x-mspublisher"}, - { "pvb", "application/vnd.3gpp.pic-bw-var"}, - { "pwn", "application/vnd.3m.post-it-notes"}, - { "pwz", "application/vnd.ms-powerpoint"}, - { "pya", "audio/vnd.ms-playready.media.pya"}, - { "pyc", "application/x-python-code"}, - { "pyo", "application/x-python-code"}, - { "py", "text/x-python"}, - { "pyv", "video/vnd.ms-playready.media.pyv"}, - { "qam", "application/vnd.epson.quickanime"}, - { "qbo", "application/vnd.intu.qbo"}, - { "qfx", "application/vnd.intu.qfx"}, - { "qps", "application/vnd.publishare-delta-tree"}, - { "qt", "video/quicktime"}, - { "qwd", "application/vnd.quark.quarkxpress"}, - { "qwt", "application/vnd.quark.quarkxpress"}, - { "qxb", "application/vnd.quark.quarkxpress"}, - { "qxd", "application/vnd.quark.quarkxpress"}, - { "qxl", "application/vnd.quark.quarkxpress"}, - { "qxt", "application/vnd.quark.quarkxpress"}, - { "ra", "audio/x-pn-realaudio"}, - { "ram", "audio/x-pn-realaudio"}, - { "rar", "application/x-rar-compressed"}, - { "ras", "image/x-cmu-raster"}, - { "rcprofile", "application/vnd.ipunplugged.rcprofile"}, - { "rdf", "application/rdf+xml"}, - { "rdz", "application/vnd.data-vision.rdz"}, - { "rep", "application/vnd.businessobjects"}, - { "res", "application/x-dtbresource+xml"}, - { "rgb", "image/x-rgb"}, - { "rif", "application/reginfo+xml"}, - { "rl", "application/resource-lists+xml"}, - { "rlc", "image/vnd.fujixerox.edmics-rlc"}, - { "rld", "application/resource-lists-diff+xml"}, - { "rm", "application/vnd.rn-realmedia"}, - { "rmi", "audio/midi"}, - { "rmp", "audio/x-pn-realaudio-plugin"}, - { "rms", "application/vnd.jcp.javame.midlet-rms"}, - { "rnc", "application/relax-ng-compact-syntax"}, - { "roff", "text/troff"}, - { "rpm", "application/x-rpm"}, - { "rpss", "application/vnd.nokia.radio-presets"}, - { "rpst", "application/vnd.nokia.radio-preset"}, - { "rq", "application/sparql-query"}, - { "rs", "application/rls-services+xml"}, - { "rsd", "application/rsd+xml"}, - { "rss", "application/rss+xml"}, - { "rtf", "application/rtf"}, - { "rtx", "text/richtext"}, - { "saf", "application/vnd.yamaha.smaf-audio"}, - { "sbml", "application/sbml+xml"}, - { "sc", "application/vnd.ibm.secure-container"}, - { "scd", "application/x-msschedule"}, - { "scm", "application/vnd.lotus-screencam"}, - { "scq", "application/scvp-cv-request"}, - { "scs", "application/scvp-cv-response"}, - { "sct", "text/scriptlet"}, - { "scurl", "text/vnd.curl.scurl"}, - { "sda", "application/vnd.stardivision.draw"}, - { "sdc", "application/vnd.stardivision.calc"}, - { "sdd", "application/vnd.stardivision.impress"}, - { "sdkd", "application/vnd.solent.sdkm+xml"}, - { "sdkm", "application/vnd.solent.sdkm+xml"}, - { "sdp", "application/sdp"}, - { "sdw", "application/vnd.stardivision.writer"}, - { "see", "application/vnd.seemail"}, - { "seed", "application/vnd.fdsn.seed"}, - { "sema", "application/vnd.sema"}, - { "semd", "application/vnd.semd"}, - { "semf", "application/vnd.semf"}, - { "ser", "application/java-serialized-object"}, - { "setpay", "application/set-payment-initiation"}, - { "setreg", "application/set-registration-initiation"}, - { "sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, - { "sfs", "application/vnd.spotfire.sfs"}, - { "sgl", "application/vnd.stardivision.writer-global"}, - { "sgml", "text/sgml"}, - { "sgm", "text/sgml"}, - { "sh", "application/x-sh"}, - { "shar", "application/x-shar"}, - { "shf", "application/shf+xml"}, - { "sic", "application/vnd.wap.sic"}, - { "sig", "application/pgp-signature"}, - { "silo", "model/mesh"}, - { "sis", "application/vnd.symbian.install"}, - { "sisx", "application/vnd.symbian.install"}, - { "sit", "application/x-stuffit"}, - { "si", "text/vnd.wap.si"}, - { "sitx", "application/x-stuffitx"}, - { "skd", "application/vnd.koan"}, - { "skm", "application/vnd.koan"}, - { "skp", "application/vnd.koan"}, - { "skt", "application/vnd.koan"}, - { "slc", "application/vnd.wap.slc"}, - { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, - { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, - { "slt", "application/vnd.epson.salt"}, - { "sl", "text/vnd.wap.sl"}, - { "smf", "application/vnd.stardivision.math"}, - { "smi", "application/smil+xml"}, - { "smil", "application/smil+xml"}, - { "snd", "audio/basic"}, - { "snf", "application/x-font-snf"}, - { "so", "application/octet-stream"}, - { "spc", "application/x-pkcs7-certificates"}, - { "spf", "application/vnd.yamaha.smaf-phrase"}, - { "spl", "application/x-futuresplash"}, - { "spot", "text/vnd.in3d.spot"}, - { "spp", "application/scvp-vp-response"}, - { "spq", "application/scvp-vp-request"}, - { "spx", "audio/ogg"}, - { "src", "application/x-wais-source"}, - { "srx", "application/sparql-results+xml"}, - { "sse", "application/vnd.kodak-descriptor"}, - { "ssf", "application/vnd.epson.ssf"}, - { "ssml", "application/ssml+xml"}, - { "sst", "application/vnd.ms-pkicertstore"}, - { "stc", "application/vnd.sun.xml.calc.template"}, - { "std", "application/vnd.sun.xml.draw.template"}, - { "s", "text/x-asm"}, - { "stf", "application/vnd.wt.stf"}, - { "sti", "application/vnd.sun.xml.impress.template"}, - { "stk", "application/hyperstudio"}, - { "stl", "application/vnd.ms-pki.stl"}, - { "stm", "text/html"}, - { "str", "application/vnd.pg.format"}, - { "stw", "application/vnd.sun.xml.writer.template"}, - { "sus", "application/vnd.sus-calendar"}, - { "susp", "application/vnd.sus-calendar"}, - { "sv4cpio", "application/x-sv4cpio"}, - { "sv4crc", "application/x-sv4crc"}, - { "svd", "application/vnd.svd"}, - { "svg", "image/svg+xml"}, - { "svgz", "image/svg+xml"}, - { "swa", "application/x-director"}, - { "swf", "application/x-shockwave-flash"}, - { "swi", "application/vnd.arastra.swi"}, - { "sxc", "application/vnd.sun.xml.calc"}, - { "sxd", "application/vnd.sun.xml.draw"}, - { "sxg", "application/vnd.sun.xml.writer.global"}, - { "sxi", "application/vnd.sun.xml.impress"}, - { "sxm", "application/vnd.sun.xml.math"}, - { "sxw", "application/vnd.sun.xml.writer"}, - { "tao", "application/vnd.tao.intent-module-archive"}, - { "t", "application/x-troff"}, - { "tar", "application/x-tar"}, - { "tcap", "application/vnd.3gpp2.tcap"}, - { "tcl", "application/x-tcl"}, - { "teacher", "application/vnd.smart.teacher"}, - { "tex", "application/x-tex"}, - { "texi", "application/x-texinfo"}, - { "texinfo", "application/x-texinfo"}, - { "text", "text/plain"}, - { "tfm", "application/x-tex-tfm"}, - { "tgz", "application/x-gzip"}, - { "tiff", "image/tiff"}, - { "tif", "image/tiff"}, - { "tmo", "application/vnd.tmobile-livetv"}, - { "torrent", "application/x-bittorrent"}, - { "tpl", "application/vnd.groove-tool-template"}, - { "tpt", "application/vnd.trid.tpt"}, - { "tra", "application/vnd.trueapp"}, - { "trm", "application/x-msterminal"}, - { "tr", "text/troff"}, - { "tsv", "text/tab-separated-values"}, - { "ttc", "application/x-font-ttf"}, - { "ttf", "application/x-font-ttf"}, - { "twd", "application/vnd.simtech-mindmapper"}, - { "twds", "application/vnd.simtech-mindmapper"}, - { "txd", "application/vnd.genomatix.tuxedo"}, - { "txf", "application/vnd.mobius.txf"}, - { "txt", "text/plain"}, - { "u32", "application/x-authorware-bin"}, - { "udeb", "application/x-debian-package"}, - { "ufd", "application/vnd.ufdl"}, - { "ufdl", "application/vnd.ufdl"}, - { "uls", "text/iuls"}, - { "umj", "application/vnd.umajin"}, - { "unityweb", "application/vnd.unity"}, - { "uoml", "application/vnd.uoml+xml"}, - { "uris", "text/uri-list"}, - { "uri", "text/uri-list"}, - { "urls", "text/uri-list"}, - { "ustar", "application/x-ustar"}, - { "utz", "application/vnd.uiq.theme"}, - { "uu", "text/x-uuencode"}, - { "vcd", "application/x-cdlink"}, - { "vcf", "text/x-vcard"}, - { "vcg", "application/vnd.groove-vcard"}, - { "vcs", "text/x-vcalendar"}, - { "vcx", "application/vnd.vcx"}, - { "vis", "application/vnd.visionary"}, - { "viv", "video/vnd.vivo"}, - { "vor", "application/vnd.stardivision.writer"}, - { "vox", "application/x-authorware-bin"}, - { "vrml", "x-world/x-vrml"}, - { "vsd", "application/vnd.visio"}, - { "vsf", "application/vnd.vsf"}, - { "vss", "application/vnd.visio"}, - { "vst", "application/vnd.visio"}, - { "vsw", "application/vnd.visio"}, - { "vtu", "model/vnd.vtu"}, - { "vxml", "application/voicexml+xml"}, - { "w3d", "application/x-director"}, - { "wad", "application/x-doom"}, - { "wav", "audio/x-wav"}, - { "wax", "audio/x-ms-wax"}, - { "wbmp", "image/vnd.wap.wbmp"}, - { "wbs", "application/vnd.criticaltools.wbs+xml"}, - { "wbxml", "application/vnd.wap.wbxml"}, - { "wcm", "application/vnd.ms-works"}, - { "wdb", "application/vnd.ms-works"}, - { "wiz", "application/msword"}, - { "wks", "application/vnd.ms-works"}, - { "wma", "audio/x-ms-wma"}, - { "wmd", "application/x-ms-wmd"}, - { "wmf", "application/x-msmetafile"}, - { "wmlc", "application/vnd.wap.wmlc"}, - { "wmlsc", "application/vnd.wap.wmlscriptc"}, - { "wmls", "text/vnd.wap.wmlscript"}, - { "wml", "text/vnd.wap.wml"}, - { "wm", "video/x-ms-wm"}, - { "wmv", "video/x-ms-wmv"}, - { "wmx", "video/x-ms-wmx"}, - { "wmz", "application/x-ms-wmz"}, - { "wpd", "application/vnd.wordperfect"}, - { "wpl", "application/vnd.ms-wpl"}, - { "wps", "application/vnd.ms-works"}, - { "wqd", "application/vnd.wqd"}, - { "wri", "application/x-mswrite"}, - { "wrl", "x-world/x-vrml"}, - { "wrz", "x-world/x-vrml"}, - { "wsdl", "application/wsdl+xml"}, - { "wspolicy", "application/wspolicy+xml"}, - { "wtb", "application/vnd.webturbo"}, - { "wvx", "video/x-ms-wvx"}, - { "x32", "application/x-authorware-bin"}, - { "x3d", "application/vnd.hzn-3d-crossword"}, - { "xaf", "x-world/x-vrml"}, - { "xap", "application/x-silverlight-app"}, - { "xar", "application/vnd.xara"}, - { "xbap", "application/x-ms-xbap"}, - { "xbd", "application/vnd.fujixerox.docuworks.binder"}, - { "xbm", "image/x-xbitmap"}, - { "xdm", "application/vnd.syncml.dm+xml"}, - { "xdp", "application/vnd.adobe.xdp+xml"}, - { "xdw", "application/vnd.fujixerox.docuworks"}, - { "xenc", "application/xenc+xml"}, - { "xer", "application/patch-ops-error+xml"}, - { "xfdf", "application/vnd.adobe.xfdf"}, - { "xfdl", "application/vnd.xfdl"}, - { "xht", "application/xhtml+xml"}, - { "xhtml", "application/xhtml+xml"}, - { "xhvml", "application/xv+xml"}, - { "xif", "image/vnd.xiff"}, - { "xla", "application/vnd.ms-excel"}, - { "xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, - { "xlb", "application/vnd.ms-excel"}, - { "xlc", "application/vnd.ms-excel"}, - { "xlm", "application/vnd.ms-excel"}, - { "xls", "application/vnd.ms-excel"}, - { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, - { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, - { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - { "xlt", "application/vnd.ms-excel"}, - { "xltm", "application/vnd.ms-excel.template.macroenabled.12"}, - { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, - { "xlw", "application/vnd.ms-excel"}, - { "xml", "application/xml"}, - { "xo", "application/vnd.olpc-sugar"}, - { "xof", "x-world/x-vrml"}, - { "xop", "application/xop+xml"}, - { "xpdl", "application/xml"}, - { "xpi", "application/x-xpinstall"}, - { "xpm", "image/x-xpixmap"}, - { "xpr", "application/vnd.is-xpr"}, - { "xps", "application/vnd.ms-xpsdocument"}, - { "xpw", "application/vnd.intercon.formnet"}, - { "xpx", "application/vnd.intercon.formnet"}, - { "xsl", "application/xml"}, - { "xslt", "application/xslt+xml"}, - { "xsm", "application/vnd.syncml+xml"}, - { "xspf", "application/xspf+xml"}, - { "xul", "application/vnd.mozilla.xul+xml"}, - { "xvm", "application/xv+xml"}, - { "xvml", "application/xv+xml"}, - { "xwd", "image/x-xwindowdump"}, - { "xyz", "chemical/x-xyz"}, - { "z", "application/x-compress"}, - { "zaz", "application/vnd.zzazz.deck+xml"}, - { "zip", "application/zip"}, - { "zir", "application/vnd.zul"}, - { "zirz", "application/vnd.zul"}, - { "zmm", "application/vnd.handheld-entertainment+xml"} + //* Do not delete the next two lines + { "", DEFAULT_ATTACHMENT_MIME_TYPE }, + { "k9s", K9_SETTINGS_MIME_TYPE}, + //* Do not delete the previous two lines + { "123", "application/vnd.lotus-1-2-3"}, + { "323", "text/h323"}, + { "3dml", "text/vnd.in3d.3dml"}, + { "3g2", "video/3gpp2"}, + { "3gp", "video/3gpp"}, + { "aab", "application/x-authorware-bin"}, + { "aac", "audio/x-aac"}, + { "aam", "application/x-authorware-map"}, + { "a", "application/octet-stream"}, + { "aas", "application/x-authorware-seg"}, + { "abw", "application/x-abiword"}, + { "acc", "application/vnd.americandynamics.acc"}, + { "ace", "application/x-ace-compressed"}, + { "acu", "application/vnd.acucobol"}, + { "acutc", "application/vnd.acucorp"}, + { "acx", "application/internet-property-stream"}, + { "adp", "audio/adpcm"}, + { "aep", "application/vnd.audiograph"}, + { "afm", "application/x-font-type1"}, + { "afp", "application/vnd.ibm.modcap"}, + { "ai", "application/postscript"}, + { "aif", "audio/x-aiff"}, + { "aifc", "audio/x-aiff"}, + { "aiff", "audio/x-aiff"}, + { "air", "application/vnd.adobe.air-application-installer-package+zip"}, + { "ami", "application/vnd.amiga.ami"}, + { "apk", "application/vnd.android.package-archive"}, + { "application", "application/x-ms-application"}, + { "apr", "application/vnd.lotus-approach"}, + { "asc", "application/pgp-signature"}, + { "asf", "video/x-ms-asf"}, + { "asm", "text/x-asm"}, + { "aso", "application/vnd.accpac.simply.aso"}, + { "asr", "video/x-ms-asf"}, + { "asx", "video/x-ms-asf"}, + { "atc", "application/vnd.acucorp"}, + { "atom", "application/atom+xml"}, + { "atomcat", "application/atomcat+xml"}, + { "atomsvc", "application/atomsvc+xml"}, + { "atx", "application/vnd.antix.game-component"}, + { "au", "audio/basic"}, + { "avi", "video/x-msvideo"}, + { "aw", "application/applixware"}, + { "axs", "application/olescript"}, + { "azf", "application/vnd.airzip.filesecure.azf"}, + { "azs", "application/vnd.airzip.filesecure.azs"}, + { "azw", "application/vnd.amazon.ebook"}, + { "bas", "text/plain"}, + { "bat", "application/x-msdownload"}, + { "bcpio", "application/x-bcpio"}, + { "bdf", "application/x-font-bdf"}, + { "bdm", "application/vnd.syncml.dm+wbxml"}, + { "bh2", "application/vnd.fujitsu.oasysprs"}, + { "bin", "application/octet-stream"}, + { "bmi", "application/vnd.bmi"}, + { "bmp", "image/bmp"}, + { "book", "application/vnd.framemaker"}, + { "box", "application/vnd.previewsystems.box"}, + { "boz", "application/x-bzip2"}, + { "bpk", "application/octet-stream"}, + { "btif", "image/prs.btif"}, + { "bz2", "application/x-bzip2"}, + { "bz", "application/x-bzip"}, + { "c4d", "application/vnd.clonk.c4group"}, + { "c4f", "application/vnd.clonk.c4group"}, + { "c4g", "application/vnd.clonk.c4group"}, + { "c4p", "application/vnd.clonk.c4group"}, + { "c4u", "application/vnd.clonk.c4group"}, + { "cab", "application/vnd.ms-cab-compressed"}, + { "car", "application/vnd.curl.car"}, + { "cat", "application/vnd.ms-pki.seccat"}, + { "cct", "application/x-director"}, + { "cc", "text/x-c"}, + { "ccxml", "application/ccxml+xml"}, + { "cdbcmsg", "application/vnd.contact.cmsg"}, + { "cdf", "application/x-cdf"}, + { "cdkey", "application/vnd.mediastation.cdkey"}, + { "cdx", "chemical/x-cdx"}, + { "cdxml", "application/vnd.chemdraw+xml"}, + { "cdy", "application/vnd.cinderella"}, + { "cer", "application/x-x509-ca-cert"}, + { "cgm", "image/cgm"}, + { "chat", "application/x-chat"}, + { "chm", "application/vnd.ms-htmlhelp"}, + { "chrt", "application/vnd.kde.kchart"}, + { "cif", "chemical/x-cif"}, + { "cii", "application/vnd.anser-web-certificate-issue-initiation"}, + { "cla", "application/vnd.claymore"}, + { "class", "application/java-vm"}, + { "clkk", "application/vnd.crick.clicker.keyboard"}, + { "clkp", "application/vnd.crick.clicker.palette"}, + { "clkt", "application/vnd.crick.clicker.template"}, + { "clkw", "application/vnd.crick.clicker.wordbank"}, + { "clkx", "application/vnd.crick.clicker"}, + { "clp", "application/x-msclip"}, + { "cmc", "application/vnd.cosmocaller"}, + { "cmdf", "chemical/x-cmdf"}, + { "cml", "chemical/x-cml"}, + { "cmp", "application/vnd.yellowriver-custom-menu"}, + { "cmx", "image/x-cmx"}, + { "cod", "application/vnd.rim.cod"}, + { "com", "application/x-msdownload"}, + { "conf", "text/plain"}, + { "cpio", "application/x-cpio"}, + { "cpp", "text/x-c"}, + { "cpt", "application/mac-compactpro"}, + { "crd", "application/x-mscardfile"}, + { "crl", "application/pkix-crl"}, + { "crt", "application/x-x509-ca-cert"}, + { "csh", "application/x-csh"}, + { "csml", "chemical/x-csml"}, + { "csp", "application/vnd.commonspace"}, + { "css", "text/css"}, + { "cst", "application/x-director"}, + { "csv", "text/csv"}, + { "c", "text/plain"}, + { "cu", "application/cu-seeme"}, + { "curl", "text/vnd.curl"}, + { "cww", "application/prs.cww"}, + { "cxt", "application/x-director"}, + { "cxx", "text/x-c"}, + { "daf", "application/vnd.mobius.daf"}, + { "dataless", "application/vnd.fdsn.seed"}, + { "davmount", "application/davmount+xml"}, + { "dcr", "application/x-director"}, + { "dcurl", "text/vnd.curl.dcurl"}, + { "dd2", "application/vnd.oma.dd2+xml"}, + { "ddd", "application/vnd.fujixerox.ddd"}, + { "deb", "application/x-debian-package"}, + { "def", "text/plain"}, + { "deploy", "application/octet-stream"}, + { "der", "application/x-x509-ca-cert"}, + { "dfac", "application/vnd.dreamfactory"}, + { "dic", "text/x-c"}, + { "diff", "text/plain"}, + { "dir", "application/x-director"}, + { "dis", "application/vnd.mobius.dis"}, + { "dist", "application/octet-stream"}, + { "distz", "application/octet-stream"}, + { "djv", "image/vnd.djvu"}, + { "djvu", "image/vnd.djvu"}, + { "dll", "application/x-msdownload"}, + { "dmg", "application/octet-stream"}, + { "dms", "application/octet-stream"}, + { "dna", "application/vnd.dna"}, + { "doc", "application/msword"}, + { "docm", "application/vnd.ms-word.document.macroenabled.12"}, + { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + { "dot", "application/msword"}, + { "dotm", "application/vnd.ms-word.template.macroenabled.12"}, + { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + { "dp", "application/vnd.osgi.dp"}, + { "dpg", "application/vnd.dpgraph"}, + { "dsc", "text/prs.lines.tag"}, + { "dtb", "application/x-dtbook+xml"}, + { "dtd", "application/xml-dtd"}, + { "dts", "audio/vnd.dts"}, + { "dtshd", "audio/vnd.dts.hd"}, + { "dump", "application/octet-stream"}, + { "dvi", "application/x-dvi"}, + { "dwf", "model/vnd.dwf"}, + { "dwg", "image/vnd.dwg"}, + { "dxf", "image/vnd.dxf"}, + { "dxp", "application/vnd.spotfire.dxp"}, + { "dxr", "application/x-director"}, + { "ecelp4800", "audio/vnd.nuera.ecelp4800"}, + { "ecelp7470", "audio/vnd.nuera.ecelp7470"}, + { "ecelp9600", "audio/vnd.nuera.ecelp9600"}, + { "ecma", "application/ecmascript"}, + { "edm", "application/vnd.novadigm.edm"}, + { "edx", "application/vnd.novadigm.edx"}, + { "efif", "application/vnd.picsel"}, + { "ei6", "application/vnd.pg.osasli"}, + { "elc", "application/octet-stream"}, + { "eml", "message/rfc822"}, + { "emma", "application/emma+xml"}, + { "eol", "audio/vnd.digital-winds"}, + { "eot", "application/vnd.ms-fontobject"}, + { "eps", "application/postscript"}, + { "epub", "application/epub+zip"}, + { "es3", "application/vnd.eszigno3+xml"}, + { "esf", "application/vnd.epson.esf"}, + { "et3", "application/vnd.eszigno3+xml"}, + { "etx", "text/x-setext"}, + { "evy", "application/envoy"}, + { "exe", "application/octet-stream"}, + { "ext", "application/vnd.novadigm.ext"}, + { "ez2", "application/vnd.ezpix-album"}, + { "ez3", "application/vnd.ezpix-package"}, + { "ez", "application/andrew-inset"}, + { "f4v", "video/x-f4v"}, + { "f77", "text/x-fortran"}, + { "f90", "text/x-fortran"}, + { "fbs", "image/vnd.fastbidsheet"}, + { "fdf", "application/vnd.fdf"}, + { "fe_launch", "application/vnd.denovo.fcselayout-link"}, + { "fg5", "application/vnd.fujitsu.oasysgp"}, + { "fgd", "application/x-director"}, + { "fh4", "image/x-freehand"}, + { "fh5", "image/x-freehand"}, + { "fh7", "image/x-freehand"}, + { "fhc", "image/x-freehand"}, + { "fh", "image/x-freehand"}, + { "fif", "application/fractals"}, + { "fig", "application/x-xfig"}, + { "fli", "video/x-fli"}, + { "flo", "application/vnd.micrografx.flo"}, + { "flr", "x-world/x-vrml"}, + { "flv", "video/x-flv"}, + { "flw", "application/vnd.kde.kivio"}, + { "flx", "text/vnd.fmi.flexstor"}, + { "fly", "text/vnd.fly"}, + { "fm", "application/vnd.framemaker"}, + { "fnc", "application/vnd.frogans.fnc"}, + { "for", "text/x-fortran"}, + { "fpx", "image/vnd.fpx"}, + { "frame", "application/vnd.framemaker"}, + { "fsc", "application/vnd.fsc.weblaunch"}, + { "fst", "image/vnd.fst"}, + { "ftc", "application/vnd.fluxtime.clip"}, + { "f", "text/x-fortran"}, + { "fti", "application/vnd.anser-web-funds-transfer-initiation"}, + { "fvt", "video/vnd.fvt"}, + { "fzs", "application/vnd.fuzzysheet"}, + { "g3", "image/g3fax"}, + { "gac", "application/vnd.groove-account"}, + { "gdl", "model/vnd.gdl"}, + { "geo", "application/vnd.dynageo"}, + { "gex", "application/vnd.geometry-explorer"}, + { "ggb", "application/vnd.geogebra.file"}, + { "ggt", "application/vnd.geogebra.tool"}, + { "ghf", "application/vnd.groove-help"}, + { "gif", "image/gif"}, + { "gim", "application/vnd.groove-identity-message"}, + { "gmx", "application/vnd.gmx"}, + { "gnumeric", "application/x-gnumeric"}, + { "gph", "application/vnd.flographit"}, + { "gqf", "application/vnd.grafeq"}, + { "gqs", "application/vnd.grafeq"}, + { "gram", "application/srgs"}, + { "gre", "application/vnd.geometry-explorer"}, + { "grv", "application/vnd.groove-injector"}, + { "grxml", "application/srgs+xml"}, + { "gsf", "application/x-font-ghostscript"}, + { "gtar", "application/x-gtar"}, + { "gtm", "application/vnd.groove-tool-message"}, + { "gtw", "model/vnd.gtw"}, + { "gv", "text/vnd.graphviz"}, + { "gz", "application/x-gzip"}, + { "h261", "video/h261"}, + { "h263", "video/h263"}, + { "h264", "video/h264"}, + { "hbci", "application/vnd.hbci"}, + { "hdf", "application/x-hdf"}, + { "hh", "text/x-c"}, + { "hlp", "application/winhlp"}, + { "hpgl", "application/vnd.hp-hpgl"}, + { "hpid", "application/vnd.hp-hpid"}, + { "hps", "application/vnd.hp-hps"}, + { "hqx", "application/mac-binhex40"}, + { "hta", "application/hta"}, + { "htc", "text/x-component"}, + { "h", "text/plain"}, + { "htke", "application/vnd.kenameaapp"}, + { "html", "text/html"}, + { "htm", "text/html"}, + { "htt", "text/webviewhtml"}, + { "hvd", "application/vnd.yamaha.hv-dic"}, + { "hvp", "application/vnd.yamaha.hv-voice"}, + { "hvs", "application/vnd.yamaha.hv-script"}, + { "icc", "application/vnd.iccprofile"}, + { "ice", "x-conference/x-cooltalk"}, + { "icm", "application/vnd.iccprofile"}, + { "ico", "image/x-icon"}, + { "ics", "text/calendar"}, + { "ief", "image/ief"}, + { "ifb", "text/calendar"}, + { "ifm", "application/vnd.shana.informed.formdata"}, + { "iges", "model/iges"}, + { "igl", "application/vnd.igloader"}, + { "igs", "model/iges"}, + { "igx", "application/vnd.micrografx.igx"}, + { "iif", "application/vnd.shana.informed.interchange"}, + { "iii", "application/x-iphone"}, + { "imp", "application/vnd.accpac.simply.imp"}, + { "ims", "application/vnd.ms-ims"}, + { "ins", "application/x-internet-signup"}, + { "in", "text/plain"}, + { "ipk", "application/vnd.shana.informed.package"}, + { "irm", "application/vnd.ibm.rights-management"}, + { "irp", "application/vnd.irepository.package+xml"}, + { "iso", "application/octet-stream"}, + { "isp", "application/x-internet-signup"}, + { "itp", "application/vnd.shana.informed.formtemplate"}, + { "ivp", "application/vnd.immervision-ivp"}, + { "ivu", "application/vnd.immervision-ivu"}, + { "jad", "text/vnd.sun.j2me.app-descriptor"}, + { "jam", "application/vnd.jam"}, + { "jar", "application/java-archive"}, + { "java", "text/x-java-source"}, + { "jfif", "image/pipeg"}, + { "jisp", "application/vnd.jisp"}, + { "jlt", "application/vnd.hp-jlyt"}, + { "jnlp", "application/x-java-jnlp-file"}, + { "joda", "application/vnd.joost.joda-archive"}, + { "jpeg", "image/jpeg"}, + { "jpe", "image/jpeg"}, + { "jpg", "image/jpeg"}, + { "jpgm", "video/jpm"}, + { "jpgv", "video/jpeg"}, + { "jpm", "video/jpm"}, + { "js", "application/x-javascript"}, + { "json", "application/json"}, + { "kar", "audio/midi"}, + { "karbon", "application/vnd.kde.karbon"}, + { "kfo", "application/vnd.kde.kformula"}, + { "kia", "application/vnd.kidspiration"}, + { "kil", "application/x-killustrator"}, + { "kml", "application/vnd.google-earth.kml+xml"}, + { "kmz", "application/vnd.google-earth.kmz"}, + { "kne", "application/vnd.kinar"}, + { "knp", "application/vnd.kinar"}, + { "kon", "application/vnd.kde.kontour"}, + { "kpr", "application/vnd.kde.kpresenter"}, + { "kpt", "application/vnd.kde.kpresenter"}, + { "ksh", "text/plain"}, + { "ksp", "application/vnd.kde.kspread"}, + { "ktr", "application/vnd.kahootz"}, + { "ktz", "application/vnd.kahootz"}, + { "kwd", "application/vnd.kde.kword"}, + { "kwt", "application/vnd.kde.kword"}, + { "latex", "application/x-latex"}, + { "lbd", "application/vnd.llamagraphics.life-balance.desktop"}, + { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, + { "les", "application/vnd.hhe.lesson-player"}, + { "lha", "application/octet-stream"}, + { "link66", "application/vnd.route66.link66+xml"}, + { "list3820", "application/vnd.ibm.modcap"}, + { "listafp", "application/vnd.ibm.modcap"}, + { "list", "text/plain"}, + { "log", "text/plain"}, + { "lostxml", "application/lost+xml"}, + { "lrf", "application/octet-stream"}, + { "lrm", "application/vnd.ms-lrm"}, + { "lsf", "video/x-la-asf"}, + { "lsx", "video/x-la-asf"}, + { "ltf", "application/vnd.frogans.ltf"}, + { "lvp", "audio/vnd.lucent.voice"}, + { "lwp", "application/vnd.lotus-wordpro"}, + { "lzh", "application/octet-stream"}, + { "m13", "application/x-msmediaview"}, + { "m14", "application/x-msmediaview"}, + { "m1v", "video/mpeg"}, + { "m2a", "audio/mpeg"}, + { "m2v", "video/mpeg"}, + { "m3a", "audio/mpeg"}, + { "m3u", "audio/x-mpegurl"}, + { "m4u", "video/vnd.mpegurl"}, + { "m4v", "video/x-m4v"}, + { "ma", "application/mathematica"}, + { "mag", "application/vnd.ecowin.chart"}, + { "maker", "application/vnd.framemaker"}, + { "man", "text/troff"}, + { "mathml", "application/mathml+xml"}, + { "mb", "application/mathematica"}, + { "mbk", "application/vnd.mobius.mbk"}, + { "mbox", "application/mbox"}, + { "mc1", "application/vnd.medcalcdata"}, + { "mcd", "application/vnd.mcd"}, + { "mcurl", "text/vnd.curl.mcurl"}, + { "mdb", "application/x-msaccess"}, + { "mdi", "image/vnd.ms-modi"}, + { "mesh", "model/mesh"}, + { "me", "text/troff"}, + { "mfm", "application/vnd.mfmp"}, + { "mgz", "application/vnd.proteus.magazine"}, + { "mht", "message/rfc822"}, + { "mhtml", "message/rfc822"}, + { "mid", "audio/midi"}, + { "midi", "audio/midi"}, + { "mif", "application/vnd.mif"}, + { "mime", "message/rfc822"}, + { "mj2", "video/mj2"}, + { "mjp2", "video/mj2"}, + { "mlp", "application/vnd.dolby.mlp"}, + { "mmd", "application/vnd.chipnuts.karaoke-mmd"}, + { "mmf", "application/vnd.smaf"}, + { "mmr", "image/vnd.fujixerox.edmics-mmr"}, + { "mny", "application/x-msmoney"}, + { "mobi", "application/x-mobipocket-ebook"}, + { "movie", "video/x-sgi-movie"}, + { "mov", "video/quicktime"}, + { "mp2a", "audio/mpeg"}, + { "mp2", "video/mpeg"}, + { "mp3", "audio/mpeg"}, + { "mp4a", "audio/mp4"}, + { "mp4s", "application/mp4"}, + { "mp4", "video/mp4"}, + { "mp4v", "video/mp4"}, + { "mpa", "video/mpeg"}, + { "mpc", "application/vnd.mophun.certificate"}, + { "mpeg", "video/mpeg"}, + { "mpe", "video/mpeg"}, + { "mpg4", "video/mp4"}, + { "mpga", "audio/mpeg"}, + { "mpg", "video/mpeg"}, + { "mpkg", "application/vnd.apple.installer+xml"}, + { "mpm", "application/vnd.blueice.multipass"}, + { "mpn", "application/vnd.mophun.application"}, + { "mpp", "application/vnd.ms-project"}, + { "mpt", "application/vnd.ms-project"}, + { "mpv2", "video/mpeg"}, + { "mpy", "application/vnd.ibm.minipay"}, + { "mqy", "application/vnd.mobius.mqy"}, + { "mrc", "application/marc"}, + { "mscml", "application/mediaservercontrol+xml"}, + { "mseed", "application/vnd.fdsn.mseed"}, + { "mseq", "application/vnd.mseq"}, + { "msf", "application/vnd.epson.msf"}, + { "msh", "model/mesh"}, + { "msi", "application/x-msdownload"}, + { "ms", "text/troff"}, + { "msty", "application/vnd.muvee.style"}, + { "mts", "model/vnd.mts"}, + { "mus", "application/vnd.musician"}, + { "musicxml", "application/vnd.recordare.musicxml+xml"}, + { "mvb", "application/x-msmediaview"}, + { "mxf", "application/mxf"}, + { "mxl", "application/vnd.recordare.musicxml"}, + { "mxml", "application/xv+xml"}, + { "mxs", "application/vnd.triscape.mxs"}, + { "mxu", "video/vnd.mpegurl"}, + { "nb", "application/mathematica"}, + { "nc", "application/x-netcdf"}, + { "ncx", "application/x-dtbncx+xml"}, + { "n-gage", "application/vnd.nokia.n-gage.symbian.install"}, + { "ngdat", "application/vnd.nokia.n-gage.data"}, + { "nlu", "application/vnd.neurolanguage.nlu"}, + { "nml", "application/vnd.enliven"}, + { "nnd", "application/vnd.noblenet-directory"}, + { "nns", "application/vnd.noblenet-sealer"}, + { "nnw", "application/vnd.noblenet-web"}, + { "npx", "image/vnd.net-fpx"}, + { "nsf", "application/vnd.lotus-notes"}, + { "nws", "message/rfc822"}, + { "oa2", "application/vnd.fujitsu.oasys2"}, + { "oa3", "application/vnd.fujitsu.oasys3"}, + { "o", "application/octet-stream"}, + { "oas", "application/vnd.fujitsu.oasys"}, + { "obd", "application/x-msbinder"}, + { "obj", "application/octet-stream"}, + { "oda", "application/oda"}, + { "odb", "application/vnd.oasis.opendocument.database"}, + { "odc", "application/vnd.oasis.opendocument.chart"}, + { "odf", "application/vnd.oasis.opendocument.formula"}, + { "odft", "application/vnd.oasis.opendocument.formula-template"}, + { "odg", "application/vnd.oasis.opendocument.graphics"}, + { "odi", "application/vnd.oasis.opendocument.image"}, + { "odp", "application/vnd.oasis.opendocument.presentation"}, + { "ods", "application/vnd.oasis.opendocument.spreadsheet"}, + { "odt", "application/vnd.oasis.opendocument.text"}, + { "oga", "audio/ogg"}, + { "ogg", "audio/ogg"}, + { "ogv", "video/ogg"}, + { "ogx", "application/ogg"}, + { "onepkg", "application/onenote"}, + { "onetmp", "application/onenote"}, + { "onetoc2", "application/onenote"}, + { "onetoc", "application/onenote"}, + { "opf", "application/oebps-package+xml"}, + { "oprc", "application/vnd.palm"}, + { "org", "application/vnd.lotus-organizer"}, + { "osf", "application/vnd.yamaha.openscoreformat"}, + { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, + { "otc", "application/vnd.oasis.opendocument.chart-template"}, + { "otf", "application/x-font-otf"}, + { "otg", "application/vnd.oasis.opendocument.graphics-template"}, + { "oth", "application/vnd.oasis.opendocument.text-web"}, + { "oti", "application/vnd.oasis.opendocument.image-template"}, + { "otm", "application/vnd.oasis.opendocument.text-master"}, + { "otp", "application/vnd.oasis.opendocument.presentation-template"}, + { "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + { "ott", "application/vnd.oasis.opendocument.text-template"}, + { "oxt", "application/vnd.openofficeorg.extension"}, + { "p10", "application/pkcs10"}, + { "p12", "application/x-pkcs12"}, + { "p7b", "application/x-pkcs7-certificates"}, + { "p7c", "application/x-pkcs7-mime"}, + { "p7m", "application/x-pkcs7-mime"}, + { "p7r", "application/x-pkcs7-certreqresp"}, + { "p7s", "application/x-pkcs7-signature"}, + { "pas", "text/x-pascal"}, + { "pbd", "application/vnd.powerbuilder6"}, + { "pbm", "image/x-portable-bitmap"}, + { "pcf", "application/x-font-pcf"}, + { "pcl", "application/vnd.hp-pcl"}, + { "pclxl", "application/vnd.hp-pclxl"}, + { "pct", "image/x-pict"}, + { "pcurl", "application/vnd.curl.pcurl"}, + { "pcx", "image/x-pcx"}, + { "pdb", "application/vnd.palm"}, + { "pdf", "application/pdf"}, + { "pfa", "application/x-font-type1"}, + { "pfb", "application/x-font-type1"}, + { "pfm", "application/x-font-type1"}, + { "pfr", "application/font-tdpfr"}, + { "pfx", "application/x-pkcs12"}, + { "pgm", "image/x-portable-graymap"}, + { "pgn", "application/x-chess-pgn"}, + { "pgp", "application/pgp-encrypted"}, + { "pic", "image/x-pict"}, + { "pkg", "application/octet-stream"}, + { "pki", "application/pkixcmp"}, + { "pkipath", "application/pkix-pkipath"}, + { "pko", "application/ynd.ms-pkipko"}, + { "plb", "application/vnd.3gpp.pic-bw-large"}, + { "plc", "application/vnd.mobius.plc"}, + { "plf", "application/vnd.pocketlearn"}, + { "pls", "application/pls+xml"}, + { "pl", "text/plain"}, + { "pma", "application/x-perfmon"}, + { "pmc", "application/x-perfmon"}, + { "pml", "application/x-perfmon"}, + { "pmr", "application/x-perfmon"}, + { "pmw", "application/x-perfmon"}, + { "png", "image/png"}, + { "pnm", "image/x-portable-anymap"}, + { "portpkg", "application/vnd.macports.portpkg"}, + { "pot,", "application/vnd.ms-powerpoint"}, + { "pot", "application/vnd.ms-powerpoint"}, + { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, + { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + { "ppa", "application/vnd.ms-powerpoint"}, + { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, + { "ppd", "application/vnd.cups-ppd"}, + { "ppm", "image/x-portable-pixmap"}, + { "pps", "application/vnd.ms-powerpoint"}, + { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, + { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + { "ppt", "application/vnd.ms-powerpoint"}, + { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, + { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + { "pqa", "application/vnd.palm"}, + { "prc", "application/x-mobipocket-ebook"}, + { "pre", "application/vnd.lotus-freelance"}, + { "prf", "application/pics-rules"}, + { "ps", "application/postscript"}, + { "psb", "application/vnd.3gpp.pic-bw-small"}, + { "psd", "image/vnd.adobe.photoshop"}, + { "psf", "application/x-font-linux-psf"}, + { "p", "text/x-pascal"}, + { "ptid", "application/vnd.pvi.ptid1"}, + { "pub", "application/x-mspublisher"}, + { "pvb", "application/vnd.3gpp.pic-bw-var"}, + { "pwn", "application/vnd.3m.post-it-notes"}, + { "pwz", "application/vnd.ms-powerpoint"}, + { "pya", "audio/vnd.ms-playready.media.pya"}, + { "pyc", "application/x-python-code"}, + { "pyo", "application/x-python-code"}, + { "py", "text/x-python"}, + { "pyv", "video/vnd.ms-playready.media.pyv"}, + { "qam", "application/vnd.epson.quickanime"}, + { "qbo", "application/vnd.intu.qbo"}, + { "qfx", "application/vnd.intu.qfx"}, + { "qps", "application/vnd.publishare-delta-tree"}, + { "qt", "video/quicktime"}, + { "qwd", "application/vnd.quark.quarkxpress"}, + { "qwt", "application/vnd.quark.quarkxpress"}, + { "qxb", "application/vnd.quark.quarkxpress"}, + { "qxd", "application/vnd.quark.quarkxpress"}, + { "qxl", "application/vnd.quark.quarkxpress"}, + { "qxt", "application/vnd.quark.quarkxpress"}, + { "ra", "audio/x-pn-realaudio"}, + { "ram", "audio/x-pn-realaudio"}, + { "rar", "application/x-rar-compressed"}, + { "ras", "image/x-cmu-raster"}, + { "rcprofile", "application/vnd.ipunplugged.rcprofile"}, + { "rdf", "application/rdf+xml"}, + { "rdz", "application/vnd.data-vision.rdz"}, + { "rep", "application/vnd.businessobjects"}, + { "res", "application/x-dtbresource+xml"}, + { "rgb", "image/x-rgb"}, + { "rif", "application/reginfo+xml"}, + { "rl", "application/resource-lists+xml"}, + { "rlc", "image/vnd.fujixerox.edmics-rlc"}, + { "rld", "application/resource-lists-diff+xml"}, + { "rm", "application/vnd.rn-realmedia"}, + { "rmi", "audio/midi"}, + { "rmp", "audio/x-pn-realaudio-plugin"}, + { "rms", "application/vnd.jcp.javame.midlet-rms"}, + { "rnc", "application/relax-ng-compact-syntax"}, + { "roff", "text/troff"}, + { "rpm", "application/x-rpm"}, + { "rpss", "application/vnd.nokia.radio-presets"}, + { "rpst", "application/vnd.nokia.radio-preset"}, + { "rq", "application/sparql-query"}, + { "rs", "application/rls-services+xml"}, + { "rsd", "application/rsd+xml"}, + { "rss", "application/rss+xml"}, + { "rtf", "application/rtf"}, + { "rtx", "text/richtext"}, + { "saf", "application/vnd.yamaha.smaf-audio"}, + { "sbml", "application/sbml+xml"}, + { "sc", "application/vnd.ibm.secure-container"}, + { "scd", "application/x-msschedule"}, + { "scm", "application/vnd.lotus-screencam"}, + { "scq", "application/scvp-cv-request"}, + { "scs", "application/scvp-cv-response"}, + { "sct", "text/scriptlet"}, + { "scurl", "text/vnd.curl.scurl"}, + { "sda", "application/vnd.stardivision.draw"}, + { "sdc", "application/vnd.stardivision.calc"}, + { "sdd", "application/vnd.stardivision.impress"}, + { "sdkd", "application/vnd.solent.sdkm+xml"}, + { "sdkm", "application/vnd.solent.sdkm+xml"}, + { "sdp", "application/sdp"}, + { "sdw", "application/vnd.stardivision.writer"}, + { "see", "application/vnd.seemail"}, + { "seed", "application/vnd.fdsn.seed"}, + { "sema", "application/vnd.sema"}, + { "semd", "application/vnd.semd"}, + { "semf", "application/vnd.semf"}, + { "ser", "application/java-serialized-object"}, + { "setpay", "application/set-payment-initiation"}, + { "setreg", "application/set-registration-initiation"}, + { "sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, + { "sfs", "application/vnd.spotfire.sfs"}, + { "sgl", "application/vnd.stardivision.writer-global"}, + { "sgml", "text/sgml"}, + { "sgm", "text/sgml"}, + { "sh", "application/x-sh"}, + { "shar", "application/x-shar"}, + { "shf", "application/shf+xml"}, + { "sic", "application/vnd.wap.sic"}, + { "sig", "application/pgp-signature"}, + { "silo", "model/mesh"}, + { "sis", "application/vnd.symbian.install"}, + { "sisx", "application/vnd.symbian.install"}, + { "sit", "application/x-stuffit"}, + { "si", "text/vnd.wap.si"}, + { "sitx", "application/x-stuffitx"}, + { "skd", "application/vnd.koan"}, + { "skm", "application/vnd.koan"}, + { "skp", "application/vnd.koan"}, + { "skt", "application/vnd.koan"}, + { "slc", "application/vnd.wap.slc"}, + { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, + { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + { "slt", "application/vnd.epson.salt"}, + { "sl", "text/vnd.wap.sl"}, + { "smf", "application/vnd.stardivision.math"}, + { "smi", "application/smil+xml"}, + { "smil", "application/smil+xml"}, + { "snd", "audio/basic"}, + { "snf", "application/x-font-snf"}, + { "so", "application/octet-stream"}, + { "spc", "application/x-pkcs7-certificates"}, + { "spf", "application/vnd.yamaha.smaf-phrase"}, + { "spl", "application/x-futuresplash"}, + { "spot", "text/vnd.in3d.spot"}, + { "spp", "application/scvp-vp-response"}, + { "spq", "application/scvp-vp-request"}, + { "spx", "audio/ogg"}, + { "src", "application/x-wais-source"}, + { "srx", "application/sparql-results+xml"}, + { "sse", "application/vnd.kodak-descriptor"}, + { "ssf", "application/vnd.epson.ssf"}, + { "ssml", "application/ssml+xml"}, + { "sst", "application/vnd.ms-pkicertstore"}, + { "stc", "application/vnd.sun.xml.calc.template"}, + { "std", "application/vnd.sun.xml.draw.template"}, + { "s", "text/x-asm"}, + { "stf", "application/vnd.wt.stf"}, + { "sti", "application/vnd.sun.xml.impress.template"}, + { "stk", "application/hyperstudio"}, + { "stl", "application/vnd.ms-pki.stl"}, + { "stm", "text/html"}, + { "str", "application/vnd.pg.format"}, + { "stw", "application/vnd.sun.xml.writer.template"}, + { "sus", "application/vnd.sus-calendar"}, + { "susp", "application/vnd.sus-calendar"}, + { "sv4cpio", "application/x-sv4cpio"}, + { "sv4crc", "application/x-sv4crc"}, + { "svd", "application/vnd.svd"}, + { "svg", "image/svg+xml"}, + { "svgz", "image/svg+xml"}, + { "swa", "application/x-director"}, + { "swf", "application/x-shockwave-flash"}, + { "swi", "application/vnd.arastra.swi"}, + { "sxc", "application/vnd.sun.xml.calc"}, + { "sxd", "application/vnd.sun.xml.draw"}, + { "sxg", "application/vnd.sun.xml.writer.global"}, + { "sxi", "application/vnd.sun.xml.impress"}, + { "sxm", "application/vnd.sun.xml.math"}, + { "sxw", "application/vnd.sun.xml.writer"}, + { "tao", "application/vnd.tao.intent-module-archive"}, + { "t", "application/x-troff"}, + { "tar", "application/x-tar"}, + { "tcap", "application/vnd.3gpp2.tcap"}, + { "tcl", "application/x-tcl"}, + { "teacher", "application/vnd.smart.teacher"}, + { "tex", "application/x-tex"}, + { "texi", "application/x-texinfo"}, + { "texinfo", "application/x-texinfo"}, + { "text", "text/plain"}, + { "tfm", "application/x-tex-tfm"}, + { "tgz", "application/x-gzip"}, + { "tiff", "image/tiff"}, + { "tif", "image/tiff"}, + { "tmo", "application/vnd.tmobile-livetv"}, + { "torrent", "application/x-bittorrent"}, + { "tpl", "application/vnd.groove-tool-template"}, + { "tpt", "application/vnd.trid.tpt"}, + { "tra", "application/vnd.trueapp"}, + { "trm", "application/x-msterminal"}, + { "tr", "text/troff"}, + { "tsv", "text/tab-separated-values"}, + { "ttc", "application/x-font-ttf"}, + { "ttf", "application/x-font-ttf"}, + { "twd", "application/vnd.simtech-mindmapper"}, + { "twds", "application/vnd.simtech-mindmapper"}, + { "txd", "application/vnd.genomatix.tuxedo"}, + { "txf", "application/vnd.mobius.txf"}, + { "txt", "text/plain"}, + { "u32", "application/x-authorware-bin"}, + { "udeb", "application/x-debian-package"}, + { "ufd", "application/vnd.ufdl"}, + { "ufdl", "application/vnd.ufdl"}, + { "uls", "text/iuls"}, + { "umj", "application/vnd.umajin"}, + { "unityweb", "application/vnd.unity"}, + { "uoml", "application/vnd.uoml+xml"}, + { "uris", "text/uri-list"}, + { "uri", "text/uri-list"}, + { "urls", "text/uri-list"}, + { "ustar", "application/x-ustar"}, + { "utz", "application/vnd.uiq.theme"}, + { "uu", "text/x-uuencode"}, + { "vcd", "application/x-cdlink"}, + { "vcf", "text/x-vcard"}, + { "vcg", "application/vnd.groove-vcard"}, + { "vcs", "text/x-vcalendar"}, + { "vcx", "application/vnd.vcx"}, + { "vis", "application/vnd.visionary"}, + { "viv", "video/vnd.vivo"}, + { "vor", "application/vnd.stardivision.writer"}, + { "vox", "application/x-authorware-bin"}, + { "vrml", "x-world/x-vrml"}, + { "vsd", "application/vnd.visio"}, + { "vsf", "application/vnd.vsf"}, + { "vss", "application/vnd.visio"}, + { "vst", "application/vnd.visio"}, + { "vsw", "application/vnd.visio"}, + { "vtu", "model/vnd.vtu"}, + { "vxml", "application/voicexml+xml"}, + { "w3d", "application/x-director"}, + { "wad", "application/x-doom"}, + { "wav", "audio/x-wav"}, + { "wax", "audio/x-ms-wax"}, + { "wbmp", "image/vnd.wap.wbmp"}, + { "wbs", "application/vnd.criticaltools.wbs+xml"}, + { "wbxml", "application/vnd.wap.wbxml"}, + { "wcm", "application/vnd.ms-works"}, + { "wdb", "application/vnd.ms-works"}, + { "wiz", "application/msword"}, + { "wks", "application/vnd.ms-works"}, + { "wma", "audio/x-ms-wma"}, + { "wmd", "application/x-ms-wmd"}, + { "wmf", "application/x-msmetafile"}, + { "wmlc", "application/vnd.wap.wmlc"}, + { "wmlsc", "application/vnd.wap.wmlscriptc"}, + { "wmls", "text/vnd.wap.wmlscript"}, + { "wml", "text/vnd.wap.wml"}, + { "wm", "video/x-ms-wm"}, + { "wmv", "video/x-ms-wmv"}, + { "wmx", "video/x-ms-wmx"}, + { "wmz", "application/x-ms-wmz"}, + { "wpd", "application/vnd.wordperfect"}, + { "wpl", "application/vnd.ms-wpl"}, + { "wps", "application/vnd.ms-works"}, + { "wqd", "application/vnd.wqd"}, + { "wri", "application/x-mswrite"}, + { "wrl", "x-world/x-vrml"}, + { "wrz", "x-world/x-vrml"}, + { "wsdl", "application/wsdl+xml"}, + { "wspolicy", "application/wspolicy+xml"}, + { "wtb", "application/vnd.webturbo"}, + { "wvx", "video/x-ms-wvx"}, + { "x32", "application/x-authorware-bin"}, + { "x3d", "application/vnd.hzn-3d-crossword"}, + { "xaf", "x-world/x-vrml"}, + { "xap", "application/x-silverlight-app"}, + { "xar", "application/vnd.xara"}, + { "xbap", "application/x-ms-xbap"}, + { "xbd", "application/vnd.fujixerox.docuworks.binder"}, + { "xbm", "image/x-xbitmap"}, + { "xdm", "application/vnd.syncml.dm+xml"}, + { "xdp", "application/vnd.adobe.xdp+xml"}, + { "xdw", "application/vnd.fujixerox.docuworks"}, + { "xenc", "application/xenc+xml"}, + { "xer", "application/patch-ops-error+xml"}, + { "xfdf", "application/vnd.adobe.xfdf"}, + { "xfdl", "application/vnd.xfdl"}, + { "xht", "application/xhtml+xml"}, + { "xhtml", "application/xhtml+xml"}, + { "xhvml", "application/xv+xml"}, + { "xif", "image/vnd.xiff"}, + { "xla", "application/vnd.ms-excel"}, + { "xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, + { "xlb", "application/vnd.ms-excel"}, + { "xlc", "application/vnd.ms-excel"}, + { "xlm", "application/vnd.ms-excel"}, + { "xls", "application/vnd.ms-excel"}, + { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, + { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, + { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + { "xlt", "application/vnd.ms-excel"}, + { "xltm", "application/vnd.ms-excel.template.macroenabled.12"}, + { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + { "xlw", "application/vnd.ms-excel"}, + { "xml", "application/xml"}, + { "xo", "application/vnd.olpc-sugar"}, + { "xof", "x-world/x-vrml"}, + { "xop", "application/xop+xml"}, + { "xpdl", "application/xml"}, + { "xpi", "application/x-xpinstall"}, + { "xpm", "image/x-xpixmap"}, + { "xpr", "application/vnd.is-xpr"}, + { "xps", "application/vnd.ms-xpsdocument"}, + { "xpw", "application/vnd.intercon.formnet"}, + { "xpx", "application/vnd.intercon.formnet"}, + { "xsl", "application/xml"}, + { "xslt", "application/xslt+xml"}, + { "xsm", "application/vnd.syncml+xml"}, + { "xspf", "application/xspf+xml"}, + { "xul", "application/vnd.mozilla.xul+xml"}, + { "xvm", "application/xv+xml"}, + { "xvml", "application/xv+xml"}, + { "xwd", "image/x-xwindowdump"}, + { "xyz", "chemical/x-xyz"}, + { "z", "application/x-compress"}, + { "zaz", "application/vnd.zzazz.deck+xml"}, + { "zip", "application/zip"}, + { "zir", "application/vnd.zul"}, + { "zirz", "application/vnd.zul"}, + { "zmm", "application/vnd.handheld-entertainment+xml"} }; /** diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index f3dbdf9cf..570cc2be5 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -16,6 +16,7 @@ import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; @@ -66,6 +67,7 @@ import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; @@ -75,6 +77,7 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; @@ -99,6 +102,8 @@ import org.apache.commons.io.IOUtils; *
*/ public class ImapStore extends Store { + public static final String STORE_TYPE = "IMAP"; + public static final int CONNECTION_SECURITY_NONE = 0; public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; @@ -131,6 +136,191 @@ public class ImapStore extends Store { private static final String[] EMPTY_STRING_ARRAY = new String[0]; + /** + * Decodes an ImapStore URI. + * + *

Possible forms:

+ *
+     * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
+     * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static ImapStoreSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String authenticationType = null; + String username = null; + String password = null; + String pathPrefix = null; + + URI imapUri; + try { + imapUri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid ImapStore URI", use); + } + + String scheme = imapUri.getScheme(); + if (scheme.equals("imap")) { + connectionSecurity = ConnectionSecurity.NONE; + port = 143; + } else if (scheme.equals("imap+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 143; + } else if (scheme.equals("imap+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 143; + } else if (scheme.equals("imap+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 993; + } else if (scheme.equals("imap+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 993; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = imapUri.getHost(); + + if (imapUri.getPort() != -1) { + port = imapUri.getPort(); + } + + if (imapUri.getUserInfo() != null) { + try { + String userinfo = imapUri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); + + if (userinfo.endsWith(":")) { + // Password is empty. This can only happen after an account was imported. + authenticationType = AuthType.valueOf(userInfoParts[0]).name(); + username = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } else if (userInfoParts.length == 2) { + authenticationType = AuthType.PLAIN.name(); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } else { + authenticationType = AuthType.valueOf(userInfoParts[0]).name(); + username = URLDecoder.decode(userInfoParts[1], "UTF-8"); + password = URLDecoder.decode(userInfoParts[2], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + String path = imapUri.getPath(); + if (path != null && path.length() > 0) { + pathPrefix = path.substring(1); + if (pathPrefix != null && pathPrefix.trim().length() == 0) { + pathPrefix = null; + } + } + + return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username, + password, pathPrefix); + } + + /** + * Creates an ImapStore URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return An ImapStore URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see ImapStore#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "imap+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "imap+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "imap+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "imap+tls+"; + break; + default: + case NONE: + scheme = "imap"; + break; + } + + AuthType authType; + try { + authType = AuthType.valueOf(server.authenticationType); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid authentication type: " + + server.authenticationType); + } + + String userInfo = authType.toString() + ":" + userEnc + ":" + passwordEnc; + try { + Map extra = server.getExtra(); + String prefix = (extra != null) ? extra.get(ImapStoreSettings.PATH_PREFIX_KEY) : null; + return new URI(scheme, userInfo, server.host, server.port, + prefix, + null, null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create ImapStore URI", e); + } + } + + /** + * This class is used to store the decoded contents of an ImapStore URI. + * + * @see ImapStore#decodeUri(String) + */ + private static class ImapStoreSettings extends ServerSettings { + private static final String PATH_PREFIX_KEY = "pathPrefix"; + + public final String pathPrefix; + + protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, + String authenticationType, String username, String password, String pathPrefix) { + super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, + password); + this.pathPrefix = pathPrefix; + } + + @Override + public Map getExtra() { + Map extra = new HashMap(); + putIfNotNull(extra, PATH_PREFIX_KEY, pathPrefix); + return extra; + } + + @Override + public ServerSettings newPassword(String newPassword) { + return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, + username, newPassword, pathPrefix); + } + } + + private String mHost; private int mPort; private String mUsername; @@ -228,74 +418,42 @@ public class ImapStore extends Store { */ private HashMap mFolderCache = new HashMap(); - /** - * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE - * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - * - * @param _uri - */ public ImapStore(Account account) throws MessagingException { super(account); - URI uri; + + ImapStoreSettings settings; try { - uri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid ImapStore URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = uri.getScheme(); - if (scheme.equals("imap")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 143; - } else if (scheme.equals("imap+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 143; - } else if (scheme.equals("imap+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 143; - } else if (scheme.equals("imap+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 993; - } else if (scheme.equals("imap+ssl")) { + break; + case SSL_TLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 993; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = uri.getHost(); + mAuthType = AuthType.valueOf(settings.authenticationType); + mUsername = settings.username; + mPassword = settings.password; - if (uri.getPort() != -1) { - mPort = uri.getPort(); - } - - if (uri.getUserInfo() != null) { - try { - String[] userInfoParts = uri.getUserInfo().split(":"); - if (userInfoParts.length == 2) { - mAuthType = AuthType.PLAIN; - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } else { - mAuthType = AuthType.valueOf(userInfoParts[0]); - mUsername = URLDecoder.decode(userInfoParts[1], "UTF-8"); - mPassword = URLDecoder.decode(userInfoParts[2], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } - - if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { - mPathPrefix = uri.getPath().substring(1); - if (mPathPrefix != null && mPathPrefix.trim().length() == 0) { - mPathPrefix = null; - } - } + mPathPrefix = settings.pathPrefix; mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); } diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 0ce234fc0..56d9b5fd8 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -26,12 +26,19 @@ import java.util.HashSet; import java.util.List; public class Pop3Store extends Store { + public static final String STORE_TYPE = "POP3"; + public static final int CONNECTION_SECURITY_NONE = 0; public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + private enum AuthType { + PLAIN, + CRAM_MD5 + } + private static final String STLS_COMMAND = "STLS"; private static final String USER_COMMAND = "USER"; private static final String PASS_COMMAND = "PASS"; @@ -52,11 +59,150 @@ public class Pop3Store extends Store { private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; + /** + * Decodes a Pop3Store URI. + * + *

Possible forms:

+ *
+     * pop3://user:password@server:port CONNECTION_SECURITY_NONE
+     * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static ServerSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String username = null; + String password = null; + + URI pop3Uri; + try { + pop3Uri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid Pop3Store URI", use); + } + + String scheme = pop3Uri.getScheme(); + if (scheme.equals("pop3")) { + connectionSecurity = ConnectionSecurity.NONE; + port = 110; + } else if (scheme.equals("pop3+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 110; + } else if (scheme.equals("pop3+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 110; + } else if (scheme.equals("pop3+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 995; + } else if (scheme.equals("pop3+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 995; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = pop3Uri.getHost(); + + if (pop3Uri.getPort() != -1) { + port = pop3Uri.getPort(); + } + + String authType = AuthType.PLAIN.name(); + if (pop3Uri.getUserInfo() != null) { + try { + int userIndex = 0, passwordIndex = 1; + String userinfo = pop3Uri.getUserInfo(); + String[] userInfoParts = userinfo.split(":"); + if (userInfoParts.length > 2 || userinfo.endsWith(":") ) { + // If 'userinfo' ends with ":" the password is empty. This can only happen + // after an account was imported (so authType and username are present). + userIndex++; + passwordIndex++; + authType = userInfoParts[0]; + } + username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); + if (userInfoParts.length > passwordIndex) { + password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username, + password); + } + + /** + * Creates a Pop3Store URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A Pop3Store URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see Pop3Store#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "pop3+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "pop3+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "pop3+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "pop3+tls+"; + break; + default: + case NONE: + scheme = "pop3"; + break; + } + + try { + AuthType.valueOf(server.authenticationType); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid authentication type (" + + server.authenticationType + ")"); + } + + String userInfo = server.authenticationType + ":" + userEnc + ":" + passwordEnc; + try { + return new URI(scheme, userInfo, server.host, server.port, null, null, + null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create Pop3Store URI", e); + } + } + + private String mHost; private int mPort; private String mUsername; private String mPassword; - private boolean useCramMd5; + private AuthType mAuthType; private int mConnectionSecurity; private HashMap mFolders = new HashMap(); private Pop3Capabilities mCapabilities; @@ -68,68 +214,41 @@ public class Pop3Store extends Store { */ private boolean mTopNotSupported; - /** - * pop3://user:password@server:port CONNECTION_SECURITY_NONE - * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - */ + public Pop3Store(Account account) throws MessagingException { super(account); - URI uri; + ServerSettings settings; try { - uri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid Pop3Store URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = uri.getScheme(); - if (scheme.equals("pop3")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 110; - } else if (scheme.equals("pop3+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 110; - } else if (scheme.equals("pop3+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 110; - } else if (scheme.equals("pop3+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 995; - } else if (scheme.equals("pop3+ssl")) { + break; + case SSL_TLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 995; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = uri.getHost(); - - if (uri.getPort() != -1) { - mPort = uri.getPort(); - } - - useCramMd5 = false; - if (uri.getUserInfo() != null) { - try { - int userIndex = 0, passwordIndex = 1; - String[] userInfoParts = uri.getUserInfo().split(":"); - if (userInfoParts.length > 2) { - userIndex++; - passwordIndex++; - useCramMd5 = true; - } - mUsername = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); - if (userInfoParts.length > passwordIndex) { - mPassword = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } + mUsername = settings.username; + mPassword = settings.password; + mAuthType = AuthType.valueOf(settings.authenticationType); } @Override @@ -249,7 +368,7 @@ public class Pop3Store extends Store { } } - if (useCramMd5) { + if (mAuthType == AuthType.CRAM_MD5) { try { String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", ""); diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 8acc2ed02..17f3cc98b 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -56,6 +56,8 @@ import java.util.zip.GZIPInputStream; * */ public class WebDavStore extends Store { + public static final String STORE_TYPE = "WebDAV"; + // Security options private static final short CONNECTION_SECURITY_NONE = 0; private static final short CONNECTION_SECURITY_TLS_OPTIONAL = 1; @@ -84,12 +86,228 @@ public class WebDavStore extends Store { private static final String DAV_MAIL_OUTBOX_FOLDER = "outbox"; private static final String DAV_MAIL_SENT_FOLDER = "sentitems"; + + /** + * Decodes a WebDavStore URI. + * + *

Possible forms:

+ *
+     * webdav://user:password@server:port CONNECTION_SECURITY_NONE
+     * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static WebDavStoreSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String username = null; + String password = null; + String alias = null; + String path = null; + String authPath = null; + String mailboxPath = null; + + + URI webDavUri; + try { + webDavUri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid WebDavStore URI", use); + } + + String scheme = webDavUri.getScheme(); + if (scheme.equals("webdav")) { + connectionSecurity = ConnectionSecurity.NONE; + } else if (scheme.equals("webdav+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + } else if (scheme.equals("webdav+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + } else if (scheme.equals("webdav+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + } else if (scheme.equals("webdav+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = webDavUri.getHost(); + if (host.startsWith("http")) { + String[] hostParts = host.split("://", 2); + if (hostParts.length > 1) { + host = hostParts[1]; + } + } + + port = webDavUri.getPort(); + + String userInfo = webDavUri.getUserInfo(); + if (userInfo != null) { + try { + String[] userInfoParts = userInfo.split(":"); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); + String userParts[] = username.split("\\\\", 2); + + if (userParts.length > 1) { + alias = userParts[1]; + } else { + alias = username; + } + if (userInfoParts.length > 1) { + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + String[] pathParts = webDavUri.getPath().split("\\|"); + for (int i = 0, count = pathParts.length; i < count; i++) { + if (i == 0) { + if (pathParts[0] != null && + pathParts[0].length() > 1) { + path = pathParts[0]; + } + } else if (i == 1) { + if (pathParts[1] != null && + pathParts[1].length() > 1) { + authPath = pathParts[1]; + } + } else if (i == 2) { + if (pathParts[2] != null && + pathParts[2].length() > 1) { + mailboxPath = pathParts[2]; + } + } + } + + return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password, + alias, path, authPath, mailboxPath); + } + + /** + * Creates a WebDavStore URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A WebDavStore URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see WebDavStore#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "webdav+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "webdav+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "webdav+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "webdav+tls+"; + break; + default: + case NONE: + scheme = "webdav"; + break; + } + + String userInfo = userEnc + ":" + passwordEnc; + + String uriPath; + Map extra = server.getExtra(); + if (extra != null) { + String path = extra.get(WebDavStoreSettings.PATH_KEY); + path = (path != null) ? path : ""; + String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY); + authPath = (authPath != null) ? authPath : ""; + String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY); + mailboxPath = (mailboxPath != null) ? mailboxPath : ""; + uriPath = path + "|" + authPath + "|" + mailboxPath; + } else { + uriPath = "||"; + } + + try { + return new URI(scheme, userInfo, server.host, server.port, uriPath, + null, null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create WebDavStore URI", e); + } + } + + + /** + * This class is used to store the decoded contents of an WebDavStore URI. + * + * @see WebDavStore#decodeUri(String) + */ + private static class WebDavStoreSettings extends ServerSettings { + private static final String ALIAS_KEY = "alias"; + private static final String PATH_KEY = "path"; + private static final String AUTH_PATH_KEY = "authPath"; + private static final String MAILBOX_PATH_KEY = "mailboxPath"; + + public final String alias; + public final String path; + public final String authPath; + public final String mailboxPath; + + protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, + String authenticationType, String username, String password, String alias, + String path, String authPath, String mailboxPath) { + super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, + password); + this.alias = alias; + this.path = path; + this.authPath = authPath; + this.mailboxPath = mailboxPath; + } + + @Override + public Map getExtra() { + Map extra = new HashMap(); + putIfNotNull(extra, ALIAS_KEY, alias); + putIfNotNull(extra, PATH_KEY, path); + putIfNotNull(extra, AUTH_PATH_KEY, authPath); + putIfNotNull(extra, MAILBOX_PATH_KEY, mailboxPath); + return extra; + } + + @Override + public ServerSettings newPassword(String newPassword) { + return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType, + username, newPassword, alias, path, authPath, mailboxPath); + } + } + + private short mConnectionSecurity; private String mUsername; /* Stores the username for authentications */ private String mAlias; /* Stores the alias for the user's mailbox */ private String mPassword; /* Stores the password for authentications */ private String mUrl; /* Stores the base URL for the server */ private String mHost; /* Stores the host name for the server */ + private int mPort; private String mPath; /* Stores the path for the server */ private String mAuthPath; /* Stores the path off of the server to post data to for form based authentication */ private String mMailboxPath; /* Stores the user specified path to the mailbox */ @@ -106,85 +324,46 @@ public class WebDavStore extends Store { private Folder mSendFolder = null; private HashMap mFolderList = new HashMap(); - /** - * webdav://user:password@server:port CONNECTION_SECURITY_NONE - * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - */ + public WebDavStore(Account account) throws MessagingException { super(account); + WebDavStoreSettings settings; try { - mUri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid WebDavStore URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } - String scheme = mUri.getScheme(); - if (scheme.equals("webdav")) { + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - } else if (scheme.equals("webdav+ssl")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - } else if (scheme.equals("webdav+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - } else if (scheme.equals("webdav+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - } else if (scheme.equals("webdav+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - } else { - throw new MessagingException("Unsupported protocol"); + break; + case SSL_TLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } - mHost = mUri.getHost(); - if (mHost.startsWith("http")) { - String[] hostParts = mHost.split("://", 2); - if (hostParts.length > 1) { - mHost = hostParts[1]; - } - } + mUsername = settings.username; + mPassword = settings.password; + mAlias = settings.alias; - if (mUri.getUserInfo() != null) { - try { - String[] userInfoParts = mUri.getUserInfo().split(":"); - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); - String userParts[] = mUsername.split("\\\\", 2); + mPath = settings.path; + mAuthPath = settings.authPath; + mMailboxPath = settings.mailboxPath; - if (userParts.length > 1) { - mAlias = userParts[1]; - } else { - mAlias = mUsername; - } - if (userInfoParts.length > 1) { - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } - } - - String[] pathParts = mUri.getPath().split("\\|"); - - for (int i = 0, count = pathParts.length; i < count; i++) { - if (i == 0) { - if (pathParts[0] != null && - pathParts[0].length() > 1) { - mPath = pathParts[0]; - } - } else if (i == 1) { - if (pathParts[1] != null && - pathParts[1].length() > 1) { - mAuthPath = pathParts[1]; - } - } else if (i == 2) { - if (pathParts[2] != null && - pathParts[2].length() > 1) { - mMailboxPath = pathParts[2]; - } - } - } if (mPath == null || mPath.equals("")) { mPath = "/Exchange"; @@ -222,7 +401,7 @@ public class WebDavStore extends Store { } else { root = "http"; } - root += "://" + mHost + ":" + mUri.getPort(); + root += "://" + mHost + ":" + mPort; return root; } diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index e7546190a..4b3c135f1 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -2,6 +2,7 @@ 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.*; import com.fsck.k9.mail.Message.RecipientType; @@ -29,14 +30,12 @@ import java.security.SecureRandom; import java.util.*; public class SmtpTransport extends Transport { + public static final String TRANSPORT_TYPE = "SMTP"; + public static final int CONNECTION_SECURITY_NONE = 0; - public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; - public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; - public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; - public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; public static final String AUTH_PLAIN = "PLAIN"; @@ -47,87 +46,187 @@ public class SmtpTransport extends Transport { public static final String AUTH_AUTOMATIC = "AUTOMATIC"; - String mHost; - - int mPort; - - String mUsername; - - String mPassword; - - String mAuthType; - - int mConnectionSecurity; - - boolean mSecure; - - Socket mSocket; - - PeekableInputStream mIn; - - OutputStream mOut; - private boolean m8bitEncodingAllowed; - - private int mLargestAcceptableMessage; /** + * Decodes a SmtpTransport URI. + * + *

Possible forms:

+ *
      * smtp://user:password@server:port CONNECTION_SECURITY_NONE
      * smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
      * smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
      * smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
      * smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
-     *
-     * @param _uri
+     * 
*/ - public SmtpTransport(String _uri) throws MessagingException { - URI uri; + public static ServerSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String authenticationType = null; + String username = null; + String password = null; + + URI smtpUri; try { - uri = new URI(_uri); + smtpUri = new URI(uri); } catch (URISyntaxException use) { - throw new MessagingException("Invalid SmtpTransport URI", use); + throw new IllegalArgumentException("Invalid SmtpTransport URI", use); } - String scheme = uri.getScheme(); + String scheme = smtpUri.getScheme(); if (scheme.equals("smtp")) { - mConnectionSecurity = CONNECTION_SECURITY_NONE; - mPort = 25; + connectionSecurity = ConnectionSecurity.NONE; + port = 25; } else if (scheme.equals("smtp+tls")) { - mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - mPort = 25; + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + port = 25; } else if (scheme.equals("smtp+tls+")) { - mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - mPort = 25; + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + port = 25; } else if (scheme.equals("smtp+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - mPort = 465; + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + port = 465; } else if (scheme.equals("smtp+ssl")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - mPort = 465; + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + port = 465; } else { - throw new MessagingException("Unsupported protocol"); + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); } - mHost = uri.getHost(); + host = smtpUri.getHost(); - if (uri.getPort() != -1) { - mPort = uri.getPort(); + if (smtpUri.getPort() != -1) { + port = smtpUri.getPort(); } - if (uri.getUserInfo() != null) { + if (smtpUri.getUserInfo() != null) { try { - String[] userInfoParts = uri.getUserInfo().split(":"); - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); + String[] userInfoParts = smtpUri.getUserInfo().split(":"); + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); if (userInfoParts.length > 1) { - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); } if (userInfoParts.length > 2) { - mAuthType = userInfoParts[2]; + authenticationType = userInfoParts[2]; } } catch (UnsupportedEncodingException enc) { // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); } } + + return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity, + authenticationType, username, password); + } + + /** + * Creates a SmtpTransport URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A SmtpTransport URI that holds the same information as the {@code server} parameter. + * + * @see Account#getTransportUri() + * @see SmtpTransport#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = (server.username != null) ? + URLEncoder.encode(server.username, "UTF-8") : ""; + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "smtp+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "smtp+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "smtp+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "smtp+tls+"; + break; + default: + case NONE: + scheme = "smtp"; + break; + } + + String authType = server.authenticationType; + if (!(AUTH_AUTOMATIC.equals(authType) || + AUTH_LOGIN.equals(authType) || + AUTH_PLAIN.equals(authType) || + AUTH_CRAM_MD5.equals(authType))) { + throw new IllegalArgumentException("Invalid authentication type: " + authType); + } + + String userInfo = userEnc + ":" + passwordEnc + ":" + authType; + try { + return new URI(scheme, userInfo, server.host, server.port, null, null, + null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create SmtpTransport URI", e); + } + } + + + String mHost; + int mPort; + String mUsername; + String mPassword; + String mAuthType; + int mConnectionSecurity; + boolean mSecure; + Socket mSocket; + PeekableInputStream mIn; + OutputStream mOut; + private boolean m8bitEncodingAllowed; + private int mLargestAcceptableMessage; + + public SmtpTransport(String uri) throws MessagingException { + ServerSettings settings; + try { + settings = decodeUri(uri); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding transport URI", e); + } + + mHost = settings.host; + mPort = settings.port; + + switch (settings.connectionSecurity) { + case NONE: + mConnectionSecurity = CONNECTION_SECURITY_NONE; + break; + case STARTTLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; + break; + case STARTTLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; + break; + case SSL_TLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; + } + + mAuthType = settings.authenticationType; + mUsername = settings.username; + mPassword = settings.password; } @Override diff --git a/src/com/fsck/k9/mail/transport/WebDavTransport.java b/src/com/fsck/k9/mail/transport/WebDavTransport.java index 72159a5e6..6b5b61d60 100644 --- a/src/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/src/com/fsck/k9/mail/transport/WebDavTransport.java @@ -7,10 +7,38 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; 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.WebDavStore; public class WebDavTransport extends Transport { + public static final String TRANSPORT_TYPE = WebDavStore.STORE_TYPE; + + /** + * Decodes a WebDavTransport URI. + * + *

+ * Note: Everything related to sending messages via WebDAV is handled by + * {@link WebDavStore}. So the transport URI is the same as the store URI. + *

+ */ + public static ServerSettings decodeUri(String uri) { + return WebDavStore.decodeUri(uri); + } + + /** + * Creates a WebDavTransport URI. + * + *

+ * Note: Everything related to sending messages via WebDAV is handled by + * {@link WebDavStore}. So the transport URI is the same as the store URI. + *

+ */ + public static String createUri(ServerSettings server) { + return WebDavStore.createUri(server); + } + + private WebDavStore store; public WebDavTransport(Account account) throws MessagingException { diff --git a/src/com/fsck/k9/preferences/AccountSettings.java b/src/com/fsck/k9/preferences/AccountSettings.java new file mode 100644 index 000000000..13c4d089a --- /dev/null +++ b/src/com/fsck/k9/preferences/AccountSettings.java @@ -0,0 +1,254 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.Account.FolderMode; +import com.fsck.k9.Account.ScrollButtons; +import com.fsck.k9.crypto.Apg; +import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.preferences.Settings.*; + +public class AccountSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("archiveFolderName", new StringSetting("Archive")); + s.put("autoExpandFolderName", new StringSetting("INBOX")); + s.put("automaticCheckIntervalMinutes", + new IntegerResourceSetting(-1, R.array.account_settings_check_frequency_values)); + s.put("chipColor", new ColorSetting(0xFF0000FF)); + s.put("cryptoApp", new StringSetting(Apg.NAME)); + s.put("cryptoAutoSignature", new BooleanSetting(false)); + s.put("defaultQuotedTextShown", new BooleanSetting(Account.DEFAULT_QUOTED_TEXT_SHOWN)); + s.put("deletePolicy", new DeletePolicySetting(Account.DELETE_POLICY_NEVER)); + s.put("displayCount", new IntegerResourceSetting(K9.DEFAULT_VISIBLE_LIMIT, + R.array.account_settings_display_count_values)); + s.put("draftsFolderName", new StringSetting("Drafts")); + s.put("enableMoveButtons", new BooleanSetting(false)); + s.put("expungePolicy", new StringResourceSetting(Account.EXPUNGE_IMMEDIATELY, + R.array.account_setup_expunge_policy_values)); + s.put("folderDisplayMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS)); + s.put("folderPushMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS)); + s.put("folderSyncMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS)); + s.put("folderTargetMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS)); + s.put("goToUnreadMessageSearch", new BooleanSetting(false)); + s.put("hideButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER)); + s.put("hideMoveButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER)); + s.put("idleRefreshMinutes", new IntegerResourceSetting(24, + R.array.idle_refresh_period_values)); + s.put("inboxFolderName", new StringSetting("INBOX")); + s.put("led", new BooleanSetting(true)); + s.put("ledColor", new ColorSetting(0xFF0000FF)); + s.put("localStorageProvider", new StorageProviderSetting()); + s.put("maxPushFolders", new IntegerRangeSetting(0, 100, 10)); + s.put("maximumAutoDownloadMessageSize", new IntegerResourceSetting(32768, + R.array.account_settings_autodownload_message_size_values)); + s.put("maximumPolledMessageAge", new IntegerResourceSetting(-1, + R.array.account_settings_message_age_values)); + s.put("messageFormat", + new EnumSetting(Account.MessageFormat.class, Account.DEFAULT_MESSAGE_FORMAT)); + s.put("messageReadReceipt", new BooleanSetting(Account.DEFAULT_MESSAGE_READ_RECEIPT)); + s.put("notificationUnreadCount", new BooleanSetting(true)); + s.put("notifyMailCheck", new BooleanSetting(false)); + s.put("notifyNewMail", new BooleanSetting(false)); + s.put("notifySelfNewMail", new BooleanSetting(true)); + s.put("pushPollOnConnect", new BooleanSetting(true)); + s.put("quotePrefix", new StringSetting(Account.DEFAULT_QUOTE_PREFIX)); + s.put("quoteStyle", + new EnumSetting(Account.QuoteStyle.class, Account.DEFAULT_QUOTE_STYLE)); + s.put("replyAfterQuote", new BooleanSetting(Account.DEFAULT_REPLY_AFTER_QUOTE)); + s.put("ring", new BooleanSetting(true)); + s.put("ringtone", new RingtoneSetting("content://settings/system/notification_sound")); + s.put("saveAllHeaders", new BooleanSetting(true)); + s.put("searchableFolders", + new EnumSetting(Account.Searchable.class, Account.Searchable.ALL)); + s.put("sentFolderName", new StringSetting("Sent")); + s.put("showPicturesEnum", + new EnumSetting(Account.ShowPictures.class, Account.ShowPictures.NEVER)); + s.put("signatureBeforeQuotedText", new BooleanSetting(false)); + s.put("spamFolderName", new StringSetting("Spam")); + s.put("subscribedFoldersOnly", new BooleanSetting(false)); + s.put("syncRemoteDeletions", new BooleanSetting(true)); + s.put("trashFolderName", new StringSetting("Trash")); + s.put("useCompression.MOBILE", new BooleanSetting(true)); + s.put("useCompression.OTHER", new BooleanSetting(true)); + s.put("useCompression.WIFI", new BooleanSetting(true)); + s.put("vibrate", new BooleanSetting(false)); + s.put("vibratePattern", new IntegerResourceSetting(0, + R.array.account_settings_vibrate_pattern_values)); + s.put("vibrateTimes", new IntegerResourceSetting(5, + R.array.account_settings_vibrate_times_label)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getAccountSettings(SharedPreferences storage, String uuid) { + Map result = new HashMap(); + String prefix = uuid + "."; + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + /** + * An integer resource setting. + * + *

+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array containing + * integer strings. + *

+ */ + public static class IntegerResourceSetting extends PseudoEnumSetting { + private final Map mMapping; + + public IntegerResourceSetting(int defaultValue, int resId) { + super(defaultValue); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(resId); + for (String value : values) { + int intValue = Integer.parseInt(value); + mapping.put(intValue, value); + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A string resource setting. + * + *

+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array. + *

+ */ + public static class StringResourceSetting extends PseudoEnumSetting { + private final Map mMapping; + + public StringResourceSetting(String defaultValue, int resId) { + super(defaultValue); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(resId); + for (String value : values) { + mapping.put(value, value); + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (!mMapping.containsKey(value)) { + throw new InvalidSettingValueException(); + } + return value; + } + } + + /** + * The notification ringtone setting. + */ + public static class RingtoneSetting extends SettingsDescription { + public RingtoneSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) { + //TODO: add validation + return value; + } + } + + /** + * The storage provider setting. + */ + public static class StorageProviderSetting extends SettingsDescription { + public StorageProviderSetting() { + super(null); + } + + @Override + public Object getDefaultValue() { + return StorageManager.getInstance(K9.app).getDefaultProviderId(); + } + + @Override + public Object fromString(String value) { + StorageManager storageManager = StorageManager.getInstance(K9.app); + Map providers = storageManager.getAvailableProviders(); + if (providers.containsKey(value)) { + return value; + } + throw new RuntimeException("Validation failed"); + } + } + + /** + * The delete policy setting. + */ + public static class DeletePolicySetting extends PseudoEnumSetting { + private Map mMapping; + + public DeletePolicySetting(int defaultValue) { + super(defaultValue); + Map mapping = new HashMap(); + mapping.put(Account.DELETE_POLICY_NEVER, "NEVER"); + mapping.put(Account.DELETE_POLICY_ON_DELETE, "DELETE"); + mapping.put(Account.DELETE_POLICY_MARK_AS_READ, "MARK_AS_READ"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer deletePolicy = Integer.parseInt(value); + if (mMapping.containsKey(deletePolicy)) { + return deletePolicy; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/FolderSettings.java b/src/com/fsck/k9/preferences/FolderSettings.java new file mode 100644 index 000000000..1a2af5502 --- /dev/null +++ b/src/com/fsck/k9/preferences/FolderSettings.java @@ -0,0 +1,44 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; + +import com.fsck.k9.mail.Folder.FolderClass; +import com.fsck.k9.preferences.Settings.*; + +public class FolderSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("displayMode", new EnumSetting(FolderClass.class, FolderClass.NO_CLASS)); + s.put("syncMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED)); + s.put("pushMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED)); + s.put("inTopGroup", new BooleanSetting(false)); + s.put("integrate", new BooleanSetting(false)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getFolderSettings(SharedPreferences storage, String uuid, + String folderName) { + Map result = new HashMap(); + String prefix = uuid + "." + folderName + "."; + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } +} diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java new file mode 100644 index 000000000..746d96dbd --- /dev/null +++ b/src/com/fsck/k9/preferences/GlobalSettings.java @@ -0,0 +1,256 @@ +package com.fsck.k9.preferences; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; +import android.os.Environment; + +import com.fsck.k9.FontSizes; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.helper.DateFormatter; +import com.fsck.k9.preferences.Settings.*; + +public class GlobalSettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("animations", new BooleanSetting(false)); + s.put("attachmentdefaultpath", + new DirectorySetting(Environment.getExternalStorageDirectory().toString())); + s.put("backgroundOperations", + new EnumSetting(K9.BACKGROUND_OPS.class, K9.BACKGROUND_OPS.WHEN_CHECKED)); + s.put("changeRegisteredNameColor", new BooleanSetting(false)); + s.put("compactLayouts", new BooleanSetting(false)); + s.put("confirmDelete", new BooleanSetting(false)); + s.put("confirmMarkAllAsRead", new BooleanSetting(false)); + s.put("confirmSpam", new BooleanSetting(false)); + s.put("countSearchMessages", new BooleanSetting(false)); + s.put("dateFormat", new DateFormatSetting(DateFormatter.DEFAULT_FORMAT)); + s.put("enableDebugLogging", new BooleanSetting(false)); + s.put("enableSensitiveLogging", new BooleanSetting(false)); + s.put("fontSizeAccountDescription", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeAccountName", new FontSizeSetting(FontSizes.MEDIUM)); + s.put("fontSizeFolderName", new FontSizeSetting(FontSizes.LARGE)); + s.put("fontSizeFolderStatus", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListDate", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListPreview", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListSender", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageListSubject", new FontSizeSetting(FontSizes.FONT_16DIP)); + s.put("fontSizeMessageViewAdditionalHeaders", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewCC", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewContent", new WebFontSizeSetting(3)); + s.put("fontSizeMessageViewDate", new FontSizeSetting(FontSizes.FONT_10DIP)); + s.put("fontSizeMessageViewSender", new FontSizeSetting(FontSizes.SMALL)); + s.put("fontSizeMessageViewSubject", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("fontSizeMessageViewTime", new FontSizeSetting(FontSizes.FONT_10DIP)); + s.put("fontSizeMessageViewTo", new FontSizeSetting(FontSizes.FONT_12DIP)); + s.put("gesturesEnabled", new BooleanSetting(true)); + s.put("hideSpecialAccounts", new BooleanSetting(false)); + s.put("keyguardPrivacy", new BooleanSetting(false)); + s.put("language", new LanguageSetting()); + s.put("manageBack", new BooleanSetting(false)); + s.put("measureAccounts", new BooleanSetting(true)); + s.put("messageListCheckboxes", new BooleanSetting(false)); + s.put("messageListPreviewLines", new IntegerRangeSetting(1, 100, 2)); + s.put("messageListStars", new BooleanSetting(true)); + s.put("messageListTouchable", new BooleanSetting(false)); + s.put("messageViewFixedWidthFont", new BooleanSetting(false)); + s.put("messageViewReturnToList", new BooleanSetting(false)); + s.put("messageViewShowNext", new BooleanSetting(false)); + s.put("mobileOptimizedLayout", new BooleanSetting(false)); + s.put("quietTimeEnabled", new BooleanSetting(false)); + s.put("quietTimeEnds", new TimeSetting("7:00")); + s.put("quietTimeStarts", new TimeSetting("21:00")); + s.put("registeredNameColor", new ColorSetting(0xFF00008F)); + s.put("showContactName", new BooleanSetting(false)); + s.put("showCorrespondentNames", new BooleanSetting(true)); + s.put("startIntegratedInbox", new BooleanSetting(false)); + s.put("theme", new ThemeSetting(android.R.style.Theme_Light)); + s.put("useGalleryBugWorkaround", new GalleryBugWorkaroundSetting()); + s.put("useVolumeKeysForListNavigation", new BooleanSetting(false)); + s.put("useVolumeKeysForNavigation", new BooleanSetting(false)); + s.put("zoomControlsEnabled", new BooleanSetting(false)); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings) { + return Settings.validate(SETTINGS, importedSettings, false); + } + + public static Map getGlobalSettings(SharedPreferences storage) { + Map result = new HashMap(); + for (String key : SETTINGS.keySet()) { + String value = storage.getString(key, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + /** + * The gallery bug work-around setting. + * + *

+ * The default value varies depending on whether you have a version of Gallery 3D installed + * that contains the bug we work around. + *

+ * + * @see K9#isGalleryBuggy() + */ + public static class GalleryBugWorkaroundSetting extends BooleanSetting { + public GalleryBugWorkaroundSetting() { + super(false); + } + + @Override + public Object getDefaultValue() { + return K9.isGalleryBuggy(); + } + } + + /** + * The language setting. + * + *

+ * Valid values are read from {@code settings_language_values} in + * {@code res/values/arrays.xml}. + *

+ */ + public static class LanguageSetting extends PseudoEnumSetting { + private final Map mMapping; + + public LanguageSetting() { + super(""); + + Map mapping = new HashMap(); + String[] values = K9.app.getResources().getStringArray(R.array.settings_language_values); + for (String value : values) { + if (value.length() == 0) { + mapping.put("", "default"); + } else { + mapping.put(value, value); + } + } + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (mMapping.containsKey(value)) { + return value; + } + + throw new InvalidSettingValueException(); + } + } + + /** + * The theme setting. + */ + public static class ThemeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public ThemeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(android.R.style.Theme_Light, "light"); + mapping.put(android.R.style.Theme, "dark"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer theme = Integer.parseInt(value); + if (mMapping.containsKey(theme)) { + return theme; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * A date format setting. + */ + public static class DateFormatSetting extends SettingsDescription { + public DateFormatSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + // The placeholders "SHORT" and "MEDIUM" are fine. + if (DateFormatter.SHORT_FORMAT.equals(value) || + DateFormatter.MEDIUM_FORMAT.equals(value)) { + return value; + } + + // If the SimpleDateFormat constructor doesn't throw an exception, we're good. + new SimpleDateFormat(value); + return value; + } catch (Exception e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A time setting. + */ + public static class TimeSetting extends SettingsDescription { + public TimeSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (!value.matches(TimePickerPreference.VALIDATION_EXPRESSION)) { + throw new InvalidSettingValueException(); + } + return value; + } + } + + /** + * A directory on the file system. + */ + public static class DirectorySetting extends SettingsDescription { + public DirectorySetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + if (new File(value).isDirectory()) { + return value; + } + } catch (Exception e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/IdentitySettings.java b/src/com/fsck/k9/preferences/IdentitySettings.java new file mode 100644 index 000000000..f796446e1 --- /dev/null +++ b/src/com/fsck/k9/preferences/IdentitySettings.java @@ -0,0 +1,99 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import android.content.SharedPreferences; + +import com.fsck.k9.EmailAddressValidator; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.preferences.Settings.*; + +public class IdentitySettings { + public static final Map SETTINGS; + + static { + Map s = new LinkedHashMap(); + + s.put("signature", new SignatureSetting()); + s.put("signatureUse", new BooleanSetting(true)); + s.put("replyTo", new OptionalEmailAddressSetting()); + + SETTINGS = Collections.unmodifiableMap(s); + } + + public static Map validate(Map importedSettings, + boolean useDefaultValues) { + return Settings.validate(SETTINGS, importedSettings, useDefaultValues); + } + + public static Map getIdentitySettings(SharedPreferences storage, String uuid, + int identityIndex) { + Map result = new HashMap(); + String prefix = uuid + "."; + String suffix = "." + Integer.toString(identityIndex); + for (String key : SETTINGS.keySet()) { + String value = storage.getString(prefix + key + suffix, null); + if (value != null) { + result.put(key, value); + } + } + return result; + } + + + public static boolean isEmailAddressValid(String email) { + return new EmailAddressValidator().isValidAddressOnly(email); + } + + /** + * The message signature setting. + */ + public static class SignatureSetting extends SettingsDescription { + public SignatureSetting() { + super(null); + } + + @Override + public Object getDefaultValue() { + return K9.app.getResources().getString(R.string.default_signature); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + return value; + } + } + + /** + * An optional email address setting. + */ + public static class OptionalEmailAddressSetting extends SettingsDescription { + private EmailAddressValidator mValidator; + + public OptionalEmailAddressSetting() { + super(null); + mValidator = new EmailAddressValidator(); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (value != null && !mValidator.isValidAddressOnly(value)) { + throw new InvalidSettingValueException(); + } + return value; + } + + @Override + public String toPrettyString(Object value) { + return (value == null) ? "" : value.toString(); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + return ("".equals(value)) ? null : fromString(value); + } + } +} diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java new file mode 100644 index 000000000..8b2b00ace --- /dev/null +++ b/src/com/fsck/k9/preferences/Settings.java @@ -0,0 +1,409 @@ +package com.fsck.k9.preferences; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import android.util.Log; + +import com.fsck.k9.FontSizes; +import com.fsck.k9.K9; + +/* + * TODO: + * - add support for different settings versions (validate old version and upgrade to new format) + * - use the default values defined in GlobalSettings and AccountSettings when creating new + * accounts + * - think of a better way to validate enums than to use the resource arrays (i.e. get rid of + * ResourceArrayValidator); maybe even use the settings description for the settings UI + * - add unit test that validates the default values are actually valid according to the validator + */ + +public class Settings { + /** + * Version number of global and account settings. + * + *

+ * This value is used as "version" attribute in the export file. It needs to be incremented + * when a global or account setting is added or removed, or when the format of a setting + * is changed (e.g. add a value to an enum). + *

+ * + * @see SettingsExporter + */ + public static final int VERSION = 1; + + public static Map validate(Map settings, + Map importedSettings, boolean useDefaultValues) { + + Map validatedSettings = new HashMap(); + for (Map.Entry setting : settings.entrySet()) { + String key = setting.getKey(); + SettingsDescription desc = setting.getValue(); + + boolean useDefaultValue; + if (!importedSettings.containsKey(key)) { + Log.v(K9.LOG_TAG, "Key \"" + key + "\" wasn't found in the imported file." + + ((useDefaultValues) ? " Using default value." : "")); + useDefaultValue = useDefaultValues; + } else { + String prettyValue = importedSettings.get(key); + try { + Object internalValue = desc.fromPrettyString(prettyValue); + String importedValue = desc.toString(internalValue); + validatedSettings.put(key, importedValue); + useDefaultValue = false; + } catch (InvalidSettingValueException e) { + Log.v(K9.LOG_TAG, "Key \"" + key + "\" has invalid value \"" + prettyValue + + "\" in imported file. " + + ((useDefaultValues) ? "Using default value." : "Skipping.")); + useDefaultValue = useDefaultValues; + } + } + + if (useDefaultValue) { + Object defaultValue = desc.getDefaultValue(); + String value = (defaultValue != null) ? desc.toString(defaultValue) : null; + validatedSettings.put(key, value); + } + } + + return validatedSettings; + } + + + /** + * Indicates an invalid setting value. + * + * @see SettingsDescription#fromString(String) + * @see SettingsDescription#fromPrettyString(String) + */ + public static class InvalidSettingValueException extends Exception { + private static final long serialVersionUID = 1L; + } + + /** + * Describes a setting. + * + *

+ * Instances of this class are used to convert the string representations of setting values to + * an internal representation (e.g. an integer) and back. + *

+ * Currently we use two different string representations: + *

+ *
    + *
  1. + * The one that is used by the internal preference {@link Storage}. It is usually obtained by + * calling {@code toString()} on the internal representation of the setting value (see e.g. + * {@link K9#save(android.content.SharedPreferences.Editor)}). + *
  2. + *
  3. + * The "pretty" version that is used by the import/export settings file (e.g. colors are + * exported in #rrggbb format instead of a integer string like "-8734021"). + *
  4. + *
+ *

+ * Note: + * For the future we should aim to get rid of the "internal" string representation. The + * "pretty" version makes reading a database dump easier and the performance impact should be + * negligible. + *

+ */ + public static abstract class SettingsDescription { + /** + * The setting's default value (internal representation). + */ + protected Object mDefaultValue; + + public SettingsDescription(Object defaultValue) { + mDefaultValue = defaultValue; + } + + /** + * Get the default value. + * + * @return The internal representation of the default value. + */ + public Object getDefaultValue() { + return mDefaultValue; + } + + /** + * Convert a setting's value to the string representation. + * + * @param value + * The internal representation of a setting's value. + * + * @return The string representation of {@code value}. + */ + public String toString(Object value) { + return value.toString(); + } + + /** + * Parse the string representation of a setting's value . + * + * @param value + * The string representation of a setting's value. + * + * @return The internal representation of the setting's value. + * + * @throws InvalidSettingValueException + * If {@code value} contains an invalid value. + */ + public abstract Object fromString(String value) throws InvalidSettingValueException; + + /** + * Convert a setting value to the "pretty" string representation. + * + * @param value + * The setting's value. + * + * @return A pretty-printed version of the setting's value. + */ + public String toPrettyString(Object value) { + return toString(value); + } + + /** + * Convert the pretty-printed version of a setting's value to the internal representation. + * + * @param value + * The pretty-printed version of the setting's value. See + * {@link #toPrettyString(Object)}. + * + * @return The internal representation of the setting's value. + * + * @throws InvalidSettingValueException + * If {@code value} contains an invalid value. + */ + public Object fromPrettyString(String value) throws InvalidSettingValueException { + return fromString(value); + } + } + + /** + * A string setting. + */ + public static class StringSetting extends SettingsDescription { + public StringSetting(String defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) { + return value; + } + } + + /** + * A boolean setting. + */ + public static class BooleanSetting extends SettingsDescription { + public BooleanSetting(boolean defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + if (Boolean.TRUE.toString().equals(value)) { + return true; + } else if (Boolean.FALSE.toString().equals(value)) { + return false; + } + throw new InvalidSettingValueException(); + } + } + + /** + * A color setting. + */ + public static class ColorSetting extends SettingsDescription { + public ColorSetting(int defaultValue) { + super(defaultValue); + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new InvalidSettingValueException(); + } + } + + @Override + public String toPrettyString(Object value) { + int color = ((Integer) value) & 0x00FFFFFF; + return String.format("#%06x", color); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + try { + if (value.length() == 7) { + return Integer.parseInt(value.substring(1), 16) | 0xFF000000; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * An {@code Enum} setting. + * + *

+ * {@link Enum#toString()} is used to obtain the "pretty" string representation. + *

+ */ + public static class EnumSetting extends SettingsDescription { + private Class> mEnumClass; + + public EnumSetting(Class> enumClass, Object defaultValue) { + super(defaultValue); + mEnumClass = enumClass; + } + + @SuppressWarnings("unchecked") + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + return Enum.valueOf((Class)mEnumClass, value); + } catch (Exception e) { + throw new InvalidSettingValueException(); + } + } + } + + /** + * A setting that has multiple valid values but doesn't use an {@link Enum} internally. + * + * @param + * The type of the internal representation (e.g. {@code Integer}). + */ + public abstract static class PseudoEnumSetting extends SettingsDescription { + public PseudoEnumSetting(Object defaultValue) { + super(defaultValue); + } + + protected abstract Map getMapping(); + + @Override + public String toPrettyString(Object value) { + return getMapping().get(value); + } + + @Override + public Object fromPrettyString(String value) throws InvalidSettingValueException { + for (Entry entry : getMapping().entrySet()) { + if (entry.getValue().equals(value)) { + return entry.getKey(); + } + } + + throw new InvalidSettingValueException(); + } + } + + /** + * A font size setting. + */ + public static class FontSizeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public FontSizeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(FontSizes.FONT_10DIP, "tiniest"); + mapping.put(FontSizes.FONT_12DIP, "tiny"); + mapping.put(FontSizes.SMALL, "smaller"); + mapping.put(FontSizes.FONT_16DIP, "small"); + mapping.put(FontSizes.MEDIUM, "medium"); + mapping.put(FontSizes.FONT_20DIP, "large"); + mapping.put(FontSizes.LARGE, "larger"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer fontSize = Integer.parseInt(value); + if (mMapping.containsKey(fontSize)) { + return fontSize; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * A {@link android.webkit.WebView} font size setting. + */ + public static class WebFontSizeSetting extends PseudoEnumSetting { + private final Map mMapping; + + public WebFontSizeSetting(int defaultValue) { + super(defaultValue); + + Map mapping = new HashMap(); + mapping.put(1, "smallest"); + mapping.put(2, "smaller"); + mapping.put(3, "normal"); + mapping.put(4, "larger"); + mapping.put(5, "largest"); + mMapping = Collections.unmodifiableMap(mapping); + } + + @Override + protected Map getMapping() { + return mMapping; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + Integer fontSize = Integer.parseInt(value); + if (mMapping.containsKey(fontSize)) { + return fontSize; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } + + /** + * An integer settings whose values a limited to a certain range. + */ + public static class IntegerRangeSetting extends SettingsDescription { + private int mStart; + private int mEnd; + + public IntegerRangeSetting(int start, int end, int defaultValue) { + super(defaultValue); + mStart = start; + mEnd = end; + } + + @Override + public Object fromString(String value) throws InvalidSettingValueException { + try { + int intValue = Integer.parseInt(value); + if (mStart <= intValue && intValue <= mEnd) { + return intValue; + } + } catch (NumberFormatException e) { /* do nothing */ } + + throw new InvalidSettingValueException(); + } + } +} diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java new file mode 100644 index 000000000..cabc8cdf2 --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsExporter.java @@ -0,0 +1,483 @@ +package com.fsck.k9.preferences; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import org.xmlpull.v1.XmlSerializer; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.util.Log; +import android.util.Xml; + +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.Store; +import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.preferences.Settings.InvalidSettingValueException; +import com.fsck.k9.preferences.Settings.SettingsDescription; + + +public class SettingsExporter { + private static final String EXPORT_FILENAME = "settings.k9s"; + + /** + * File format version number. + * + *

+ * Increment this if you need to change the structure of the settings file. When you do this + * remember that we also have to be able to handle old file formats. So have fun adding support + * for that to {@link SettingsImporter} :) + *

+ */ + public static final int FILE_FORMAT_VERSION = 1; + + public static final String ROOT_ELEMENT = "k9settings"; + public static final String VERSION_ATTRIBUTE = "version"; + public static final String FILE_FORMAT_ATTRIBUTE = "format"; + public static final String GLOBAL_ELEMENT = "global"; + public static final String SETTINGS_ELEMENT = "settings"; + public static final String ACCOUNTS_ELEMENT = "accounts"; + public static final String ACCOUNT_ELEMENT = "account"; + public static final String UUID_ATTRIBUTE = "uuid"; + public static final String INCOMING_SERVER_ELEMENT = "incoming-server"; + public static final String OUTGOING_SERVER_ELEMENT = "outgoing-server"; + public static final String TYPE_ATTRIBUTE = "type"; + public static final String HOST_ELEMENT = "host"; + public static final String PORT_ELEMENT = "port"; + public static final String CONNECTION_SECURITY_ELEMENT = "connection-security"; + public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type"; + public static final String USERNAME_ELEMENT = "username"; + public static final String PASSWORD_ELEMENT = "password"; + public static final String EXTRA_ELEMENT = "extra"; + public static final String IDENTITIES_ELEMENT = "identities"; + public static final String IDENTITY_ELEMENT = "identity"; + public static final String FOLDERS_ELEMENT = "folders"; + public static final String FOLDER_ELEMENT = "folder"; + public static final String NAME_ATTRIBUTE = "name"; + public static final String VALUE_ELEMENT = "value"; + public static final String KEY_ATTRIBUTE = "key"; + public static final String NAME_ELEMENT = "name"; + public static final String EMAIL_ELEMENT = "email"; + public static final String DESCRIPTION_ELEMENT = "description"; + + + public static String exportToFile(Context context, boolean includeGlobals, + Set accountUuids) + throws SettingsImportExportException { + + OutputStream os = null; + String filename = null; + try + { + File dir = new File(Environment.getExternalStorageDirectory() + File.separator + + context.getPackageName()); + dir.mkdirs(); + File file = Utility.createUniqueFile(dir, EXPORT_FILENAME); + filename = file.getAbsolutePath(); + os = new FileOutputStream(filename); + + exportPreferences(context, os, includeGlobals, accountUuids); + + // If all went well, we return the name of the file just written. + return filename; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException ioe) { + Log.w(K9.LOG_TAG, "Couldn't close exported settings file: " + filename); + } + } + } + } + + public static void exportPreferences(Context context, OutputStream os, boolean includeGlobals, + Set accountUuids) throws SettingsImportExportException { + + try { + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(os, "UTF-8"); + + serializer.startDocument(null, Boolean.valueOf(true)); + + // Output with indentation + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + serializer.startTag(null, ROOT_ELEMENT); + serializer.attribute(null, VERSION_ATTRIBUTE, Integer.toString(Settings.VERSION)); + serializer.attribute(null, FILE_FORMAT_ATTRIBUTE, + Integer.toString(FILE_FORMAT_VERSION)); + + Log.i(K9.LOG_TAG, "Exporting preferences"); + + Preferences preferences = Preferences.getPreferences(context); + SharedPreferences storage = preferences.getPreferences(); + + Set exportAccounts; + if (accountUuids == null) { + Account[] accounts = preferences.getAccounts(); + exportAccounts = new HashSet(); + for (Account account : accounts) { + exportAccounts.add(account.getUuid()); + } + } else { + exportAccounts = accountUuids; + } + + Map prefs = new TreeMap(storage.getAll()); + + if (includeGlobals) { + serializer.startTag(null, GLOBAL_ELEMENT); + writeSettings(serializer, prefs); + serializer.endTag(null, GLOBAL_ELEMENT); + } + + serializer.startTag(null, ACCOUNTS_ELEMENT); + for (String accountUuid : exportAccounts) { + Account account = preferences.getAccount(accountUuid); + writeAccount(serializer, account, prefs); + } + serializer.endTag(null, ACCOUNTS_ELEMENT); + + serializer.endTag(null, ROOT_ELEMENT); + serializer.endDocument(); + serializer.flush(); + + } catch (Exception e) { + throw new SettingsImportExportException(e.getLocalizedMessage(), e); + } + } + + private static void writeSettings(XmlSerializer serializer, + Map prefs) throws IOException { + + for (String key : GlobalSettings.SETTINGS.keySet()) { + String valueString = (String) prefs.get(key); + if (valueString != null) { + try { + SettingsDescription setting = GlobalSettings.SETTINGS.get(key); + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, key, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Global setting \"" + key + "\" has invalid value \"" + + valueString + "\" in preference storage. This shouldn't happen!"); + } + } else { + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Couldn't find key \"" + key + "\" in preference storage." + + "Using default value."); + } + + SettingsDescription setting = GlobalSettings.SETTINGS.get(key); + Object value = setting.getDefaultValue(); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, key, outputValue); + } + } + } + + private static void writeAccount(XmlSerializer serializer, Account account, + Map prefs) throws IOException { + + Set identities = new HashSet(); + Set folders = new HashSet(); + String accountUuid = account.getUuid(); + + serializer.startTag(null, ACCOUNT_ELEMENT); + serializer.attribute(null, UUID_ATTRIBUTE, accountUuid); + + String name = (String) prefs.get(accountUuid + "." + Account.ACCOUNT_DESCRIPTION_KEY); + if (name != null) { + serializer.startTag(null, NAME_ELEMENT); + serializer.text(name); + serializer.endTag(null, NAME_ELEMENT); + } + + + // Write incoming server settings + ServerSettings incoming = Store.decodeStoreUri(account.getStoreUri()); + serializer.startTag(null, INCOMING_SERVER_ELEMENT); + serializer.attribute(null, TYPE_ATTRIBUTE, incoming.type); + + writeElement(serializer, HOST_ELEMENT, incoming.host); + if (incoming.port != -1) { + writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port)); + } + writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name()); + writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType); + writeElement(serializer, USERNAME_ELEMENT, incoming.username); + // XXX For now we don't export the password + //writeElement(serializer, PASSWORD_ELEMENT, incoming.password); + + Map extras = incoming.getExtra(); + if (extras != null && extras.size() > 0) { + serializer.startTag(null, EXTRA_ELEMENT); + for (Entry extra : extras.entrySet()) { + writeKeyValue(serializer, extra.getKey(), extra.getValue()); + } + serializer.endTag(null, EXTRA_ELEMENT); + } + + serializer.endTag(null, INCOMING_SERVER_ELEMENT); + + + // Write outgoing server settings + ServerSettings outgoing = Transport.decodeTransportUri(account.getTransportUri()); + serializer.startTag(null, OUTGOING_SERVER_ELEMENT); + serializer.attribute(null, TYPE_ATTRIBUTE, outgoing.type); + + writeElement(serializer, HOST_ELEMENT, outgoing.host); + if (outgoing.port != -1) { + writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port)); + } + writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name()); + writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType); + writeElement(serializer, USERNAME_ELEMENT, outgoing.username); + // XXX For now we don't export the password + //writeElement(serializer, PASSWORD_ELEMENT, outgoing.password); + + extras = outgoing.getExtra(); + if (extras != null && extras.size() > 0) { + serializer.startTag(null, EXTRA_ELEMENT); + for (Entry extra : extras.entrySet()) { + writeKeyValue(serializer, extra.getKey(), extra.getValue()); + } + serializer.endTag(null, EXTRA_ELEMENT); + } + + serializer.endTag(null, OUTGOING_SERVER_ELEMENT); + + + // Write account settings + serializer.startTag(null, SETTINGS_ELEMENT); + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 2) { + // Skip global settings + continue; + } + + String keyUuid = comps[0]; + String secondPart = comps[1]; + + if (!keyUuid.equals(accountUuid)) { + // Setting doesn't belong to the account we're currently writing. + continue; + } + + String keyPart; + if (comps.length >= 3) { + String thirdPart = comps[2]; + + if (Account.IDENTITY_DESCRIPTION_KEY.equals(secondPart)) { + // This is an identity key. Save identity index for later... + try { + identities.add(Integer.parseInt(thirdPart)); + } catch (NumberFormatException e) { /* ignore */ } + // ... but don't write it now. + continue; + } + + if (FolderSettings.SETTINGS.containsKey(thirdPart)) { + // This is a folder key. Save folder name for later... + folders.add(secondPart); + // ... but don't write it now. + continue; + } + + // Strip account UUID from key + keyPart = key.substring(comps[0].length() + 1); + } else { + keyPart = secondPart; + } + + SettingsDescription setting = AccountSettings.SETTINGS.get(keyPart); + if (setting != null) { + // Only export account settings that can be found in AccountSettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String pretty = setting.toPrettyString(value); + writeKeyValue(serializer, keyPart, pretty); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Account setting \"" + keyPart + "\" (" + + account.getDescription() + ") has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + serializer.endTag(null, SETTINGS_ELEMENT); + + if (identities.size() > 0) { + serializer.startTag(null, IDENTITIES_ELEMENT); + + // Sort identity indices (that's why we store them as Integers) + List sortedIdentities = new ArrayList(identities); + Collections.sort(sortedIdentities); + + for (Integer identityIndex : sortedIdentities) { + writeIdentity(serializer, accountUuid, identityIndex.toString(), prefs); + } + serializer.endTag(null, IDENTITIES_ELEMENT); + } + + if (folders.size() > 0) { + serializer.startTag(null, FOLDERS_ELEMENT); + for (String folder : folders) { + writeFolder(serializer, accountUuid, folder, prefs); + } + serializer.endTag(null, FOLDERS_ELEMENT); + } + + serializer.endTag(null, ACCOUNT_ELEMENT); + } + + private static void writeIdentity(XmlSerializer serializer, String accountUuid, + String identity, Map prefs) throws IOException { + + serializer.startTag(null, IDENTITY_ELEMENT); + + String prefix = accountUuid + "."; + String suffix = "." + identity; + + // Write name belonging to the identity + String name = (String) prefs.get(prefix + Account.IDENTITY_NAME_KEY + suffix); + serializer.startTag(null, NAME_ELEMENT); + serializer.text(name); + serializer.endTag(null, NAME_ELEMENT); + + // Write email address belonging to the identity + String email = (String) prefs.get(prefix + Account.IDENTITY_EMAIL_KEY + suffix); + serializer.startTag(null, EMAIL_ELEMENT); + serializer.text(email); + serializer.endTag(null, EMAIL_ELEMENT); + + // Write identity description + String description = (String) prefs.get(prefix + Account.IDENTITY_DESCRIPTION_KEY + suffix); + if (description != null) { + serializer.startTag(null, DESCRIPTION_ELEMENT); + serializer.text(description); + serializer.endTag(null, DESCRIPTION_ELEMENT); + } + + // Write identity settings + serializer.startTag(null, SETTINGS_ELEMENT); + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 3) { + // Skip non-identity config entries + continue; + } + + String keyUuid = comps[0]; + String identityKey = comps[1]; + String identityIndex = comps[2]; + if (!keyUuid.equals(accountUuid) || !identityIndex.equals(identity)) { + // Skip entries that belong to another identity + continue; + } + + SettingsDescription setting = IdentitySettings.SETTINGS.get(identityKey); + if (setting != null) { + // Only write settings that have an entry in IdentitySettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, identityKey, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Identity setting \"" + identityKey + + "\" has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + serializer.endTag(null, SETTINGS_ELEMENT); + + serializer.endTag(null, IDENTITY_ELEMENT); + } + + private static void writeFolder(XmlSerializer serializer, String accountUuid, + String folder, Map prefs) throws IOException { + + serializer.startTag(null, FOLDER_ELEMENT); + serializer.attribute(null, NAME_ATTRIBUTE, folder); + + // Write folder settings + for (Map.Entry entry : prefs.entrySet()) { + String key = entry.getKey(); + String valueString = entry.getValue().toString(); + String[] comps = key.split("\\."); + + if (comps.length < 3) { + // Skip non-folder config entries + continue; + } + + String keyUuid = comps[0]; + String folderName = comps[1]; + String folderKey = comps[2]; + + if (!keyUuid.equals(accountUuid) || !folderName.equals(folder)) { + // Skip entries that belong to another folder + continue; + } + + SettingsDescription setting = FolderSettings.SETTINGS.get(folderKey); + if (setting != null) { + // Only write settings that have an entry in FolderSettings.SETTINGS + try { + Object value = setting.fromString(valueString); + String outputValue = setting.toPrettyString(value); + writeKeyValue(serializer, folderKey, outputValue); + } catch (InvalidSettingValueException e) { + Log.w(K9.LOG_TAG, "Folder setting \"" + folderKey + + "\" has invalid value \"" + valueString + + "\" in preference storage. This shouldn't happen!"); + } + } + } + + serializer.endTag(null, FOLDER_ELEMENT); + } + + private static void writeElement(XmlSerializer serializer, String elementName, String value) + throws IllegalArgumentException, IllegalStateException, IOException { + if (value != null) { + serializer.startTag(null, elementName); + serializer.text(value); + serializer.endTag(null, elementName); + } + } + + private static void writeKeyValue(XmlSerializer serializer, String key, String value) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(null, VALUE_ELEMENT); + serializer.attribute(null, KEY_ATTRIBUTE, key); + if (value != null) { + serializer.text(value); + } + serializer.endTag(null, VALUE_ELEMENT); + } +} diff --git a/src/com/fsck/k9/preferences/SettingsImportExportException.java b/src/com/fsck/k9/preferences/SettingsImportExportException.java new file mode 100644 index 000000000..616a466bf --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsImportExportException.java @@ -0,0 +1,21 @@ +package com.fsck.k9.preferences; + +public class SettingsImportExportException extends Exception { + + public SettingsImportExportException() { + super(); + } + + public SettingsImportExportException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public SettingsImportExportException(String detailMessage) { + super(detailMessage); + } + + public SettingsImportExportException(Throwable throwable) { + super(throwable); + } + +} diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java new file mode 100644 index 000000000..3c1b36475 --- /dev/null +++ b/src/com/fsck/k9/preferences/SettingsImporter.java @@ -0,0 +1,1106 @@ +package com.fsck.k9.preferences; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +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.DateFormatter; +import com.fsck.k9.helper.Utility; +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.WebDavStore; +import com.fsck.k9.preferences.Settings.InvalidSettingValueException; + +public class SettingsImporter { + + /** + * Class to list the contents of an import file/stream. + * + * @see SettingsImporter#getImportStreamContents(InputStream) + */ + public static class ImportContents { + /** + * True, if the import file contains global settings. + */ + public final boolean globalSettings; + + /** + * The list of accounts found in the import file. Never {@code null}. + */ + public final List accounts; + + private ImportContents(boolean globalSettings, List accounts) { + this.globalSettings = globalSettings; + this.accounts = accounts; + } + } + + /** + * Class to describe an account (name, UUID). + * + * @see ImportContents + */ + public static class AccountDescription { + /** + * The name of the account. + */ + public final String name; + + /** + * The UUID of the account. + */ + public final String uuid; + + private AccountDescription(String name, String uuid) { + this.name = name; + this.uuid = uuid; + } + } + + public static class AccountDescriptionPair { + public final AccountDescription original; + public final AccountDescription imported; + + private AccountDescriptionPair(AccountDescription original, AccountDescription imported) { + this.original = original; + this.imported = imported; + } + } + + public static class ImportResults { + public final boolean globalSettings; + public final List importedAccounts; + public final List errorneousAccounts; + + private ImportResults(boolean globalSettings, + List importedAccounts, + List errorneousAccounts) { + this.globalSettings = globalSettings; + this.importedAccounts = importedAccounts; + this.errorneousAccounts = errorneousAccounts; + } + } + + /** + * Parses an import {@link InputStream} and returns information on whether it contains global + * settings and/or account settings. For all account configurations found, the name of the + * account along with the account UUID is returned. + * + * @param inputStream + * An {@code InputStream} to read the settings from. + * + * @return An {@link ImportContents} instance containing information about the contents of the + * settings file. + * + * @throws SettingsImportExportException + * In case of an error. + */ + public static ImportContents getImportStreamContents(InputStream inputStream) + throws SettingsImportExportException { + + try { + // Parse the import stream but don't save individual settings (overview=true) + Imported imported = parseSettings(inputStream, false, null, true); + + // If the stream contains global settings the "globalSettings" member will not be null + boolean globalSettings = (imported.globalSettings != null); + + final List accounts = new ArrayList(); + // If the stream contains at least one account configuration the "accounts" member + // will not be null. + if (imported.accounts != null) { + for (ImportedAccount account : imported.accounts.values()) { + accounts.add(new AccountDescription(account.name, account.uuid)); + } + } + + //TODO: throw exception if neither global settings nor account settings could be found + + return new ImportContents(globalSettings, accounts); + + } catch (SettingsImportExportException e) { + throw e; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + /** + * Reads an import {@link InputStream} and imports the global settings and/or account + * configurations specified by the arguments. + * + * @param context + * A {@link Context} instance. + * @param inputStream + * The {@code InputStream} to read the settings from. + * @param globalSettings + * {@code true} if global settings should be imported from the file. + * @param accountUuids + * A list of UUIDs of the accounts that should be imported. + * @param overwrite + * {@code true} if existing accounts should be overwritten when an account with the + * same UUID is found in the settings file.
+ * Note: This can have side-effects we currently don't handle, e.g. + * changing the account type from IMAP to POP3. So don't use this for now! + * + * @return An {@link ImportResults} instance containing information about errors and + * successfully imported accounts. + * + * @throws SettingsImportExportException + * In case of an error. + */ + public static ImportResults importSettings(Context context, InputStream inputStream, + boolean globalSettings, List accountUuids, boolean overwrite) + throws SettingsImportExportException { + + try + { + boolean globalSettingsImported = false; + List importedAccounts = new ArrayList(); + List errorneousAccounts = new ArrayList(); + + Imported imported = parseSettings(inputStream, globalSettings, accountUuids, false); + + Preferences preferences = Preferences.getPreferences(context); + SharedPreferences storage = preferences.getPreferences(); + + if (globalSettings) { + try { + SharedPreferences.Editor editor = storage.edit(); + if (imported.globalSettings != null) { + importGlobalSettings(storage, editor, imported.globalSettings); + } else { + Log.w(K9.LOG_TAG, "Was asked to import global settings but none found."); + } + if (editor.commit()) { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Committed global settings to the preference " + + "storage."); + } + globalSettingsImported = true; + } else { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Failed to commit global settings to the " + + "preference storage"); + } + } + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Exception while importing global settings", e); + } + } + + if (accountUuids != null && accountUuids.size() > 0) { + if (imported.accounts != null) { + List newUuids = new ArrayList(); + for (String accountUuid : accountUuids) { + if (imported.accounts.containsKey(accountUuid)) { + ImportedAccount account = imported.accounts.get(accountUuid); + try { + SharedPreferences.Editor editor = storage.edit(); + + AccountDescriptionPair importResult = importAccount(context, + editor, account, overwrite); + + String newUuid = importResult.imported.uuid; + if (!newUuid.equals(importResult.original.uuid)) { + newUuids.add(newUuid); + } + if (editor.commit()) { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Committed settings for account \"" + + importResult.imported.name + + "\" to the settings database."); + } + importedAccounts.add(importResult); + } else { + if (K9.DEBUG) { + Log.w(K9.LOG_TAG, "Error while committing settings for " + + "account \"" + importResult.original.name + + "\" to the settings database."); + } + errorneousAccounts.add(importResult.original); + } + } catch (InvalidSettingValueException e) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Encountered invalid setting while " + + "importing account \"" + account.name + "\"", e); + } + errorneousAccounts.add(new AccountDescription(account.name, account.uuid)); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Exception while importing account \"" + + account.name + "\"", e); + errorneousAccounts.add(new AccountDescription(account.name, account.uuid)); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import account with UUID " + + accountUuid + ". But this account wasn't found."); + } + } + + SharedPreferences.Editor editor = storage.edit(); + + if (newUuids.size() > 0) { + String oldAccountUuids = storage.getString("accountUuids", ""); + String appendUuids = Utility.combine(newUuids.toArray(new String[0]), ','); + String prefix = ""; + if (oldAccountUuids.length() > 0) { + prefix = oldAccountUuids + ","; + } + putString(editor, "accountUuids", prefix + appendUuids); + } + + String defaultAccountUuid = storage.getString("defaultAccountUuid", null); + if (defaultAccountUuid == null) { + putString(editor, "defaultAccountUuid", accountUuids.get(0)); + } + + if (!editor.commit()) { + throw new SettingsImportExportException("Failed to set default account"); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import at least one account but none found."); + } + } + + preferences.loadAccounts(); + DateFormatter.clearChosenFormat(); + K9.loadPrefs(preferences); + K9.setServicesEnabled(context); + + return new ImportResults(globalSettingsImported, importedAccounts, errorneousAccounts); + + } catch (SettingsImportExportException e) { + throw e; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + private static void importGlobalSettings(SharedPreferences storage, + SharedPreferences.Editor editor, ImportedSettings settings) { + + Map validatedSettings = GlobalSettings.validate(settings.settings); + + // Use current global settings as base and overwrite with validated settings read from the + // import file. + Map mergedSettings = + new HashMap(GlobalSettings.getGlobalSettings(storage)); + mergedSettings.putAll(validatedSettings); + + for (Map.Entry setting : mergedSettings.entrySet()) { + String key = setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + } + + private static AccountDescriptionPair importAccount(Context context, + SharedPreferences.Editor editor, ImportedAccount account, boolean overwrite) + throws InvalidSettingValueException { + + AccountDescription original = new AccountDescription(account.name, account.uuid); + + Preferences prefs = Preferences.getPreferences(context); + Account[] accounts = prefs.getAccounts(); + + String uuid = account.uuid; + Account existingAccount = prefs.getAccount(uuid); + boolean mergeImportedAccount = (overwrite && existingAccount != null); + + if (!overwrite && existingAccount != null) { + // An account with this UUID already exists, but we're not allowed to overwrite it. + // So generate a new UUID. + uuid = UUID.randomUUID().toString(); + } + + // Make sure the account name is unique + String accountName = (account.name != null) ? account.name : "Imported"; + if (isAccountNameUsed(accountName, accounts)) { + // Account name is already in use. So generate a new one by appending " (x)", where x + // is the first number >= 1 that results in an unused account name. + for (int i = 1; i <= accounts.length; i++) { + accountName = account.name + " (" + i + ")"; + if (!isAccountNameUsed(accountName, accounts)) { + break; + } + } + } + + // Write account name + String accountKeyPrefix = uuid + "."; + putString(editor, accountKeyPrefix + Account.ACCOUNT_DESCRIPTION_KEY, accountName); + + if (account.incoming == null) { + // We don't import accounts without incoming server settings + throw new InvalidSettingValueException(); + } + + // 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)); + + // Mark account as disabled if the settings file didn't contain a password + boolean createAccountDisabled = (incoming.password == null || + incoming.password.length() == 0); + + if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) { + // All account types except WebDAV need to provide outgoing server settings + throw new InvalidSettingValueException(); + } + + if (account.outgoing != null) { + // 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)); + + // Mark account as disabled if the settings file didn't contain a password + if (outgoing.password == null || outgoing.password.length() == 0) { + createAccountDisabled = true; + } + } + + // Write key to mark account as disabled if necessary + if (createAccountDisabled) { + editor.putBoolean(accountKeyPrefix + "enabled", false); + } + + // Validate account settings + Map validatedSettings = + AccountSettings.validate(account.settings.settings, !mergeImportedAccount); + + // Merge account settings if necessary + Map writeSettings; + if (mergeImportedAccount) { + writeSettings = new HashMap( + AccountSettings.getAccountSettings(prefs.getPreferences(), uuid)); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write account settings + for (Map.Entry setting : writeSettings.entrySet()) { + String key = accountKeyPrefix + setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + + // If it's a new account generate and write a new "accountNumber" + if (!mergeImportedAccount) { + int newAccountNumber = Account.generateAccountNumber(prefs); + putString(editor, accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber)); + } + + // Write identities + if (account.identities != null) { + importIdentities(editor, uuid, account, overwrite, existingAccount, prefs); + } else if (!mergeImportedAccount) { + // Require accounts to at least have one identity + throw new InvalidSettingValueException(); + } + + // Write folder settings + if (account.folders != null) { + for (ImportedFolder folder : account.folders) { + importFolder(editor, uuid, folder, mergeImportedAccount, prefs); + } + } + + //TODO: sync folder settings with localstore? + + AccountDescription imported = new AccountDescription(accountName, uuid); + return new AccountDescriptionPair(original, imported); + } + + private static void importFolder(SharedPreferences.Editor editor, String uuid, + ImportedFolder folder, boolean overwrite, Preferences prefs) { + + // Validate folder settings + Map validatedSettings = + FolderSettings.validate(folder.settings.settings, !overwrite); + + // Merge folder settings if necessary + Map writeSettings; + if (overwrite) { + writeSettings = FolderSettings.getFolderSettings(prefs.getPreferences(), + uuid, folder.name); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write folder settings + String prefix = uuid + "." + folder.name + "."; + for (Map.Entry setting : writeSettings.entrySet()) { + String key = prefix + setting.getKey(); + String value = setting.getValue(); + putString(editor, key, value); + } + } + + private static void importIdentities(SharedPreferences.Editor editor, String uuid, + ImportedAccount account, boolean overwrite, Account existingAccount, + Preferences prefs) throws InvalidSettingValueException { + + String accountKeyPrefix = uuid + "."; + + // Gather information about existing identities for this account (if any) + int nextIdentityIndex = 0; + final List existingIdentities; + if (overwrite && existingAccount != null) { + existingIdentities = existingAccount.getIdentities(); + nextIdentityIndex = existingIdentities.size(); + } else { + existingIdentities = new ArrayList(); + } + + // Write identities + for (ImportedIdentity identity : account.identities) { + int writeIdentityIndex = nextIdentityIndex; + boolean mergeSettings = false; + if (overwrite && existingIdentities.size() > 0) { + int identityIndex = findIdentity(identity, existingIdentities); + if (identityIndex != -1) { + writeIdentityIndex = identityIndex; + mergeSettings = true; + } + } + if (!mergeSettings) { + nextIdentityIndex++; + } + + String identityDescription = (identity.description == null) ? + "Imported" : identity.description; + if (isIdentityDescriptionUsed(identityDescription, existingIdentities)) { + // Identity description is already in use. So generate a new one by appending + // " (x)", where x is the first number >= 1 that results in an unused identity + // description. + for (int i = 1; i <= existingIdentities.size(); i++) { + identityDescription = identity.description + " (" + i + ")"; + if (!isIdentityDescriptionUsed(identityDescription, existingIdentities)) { + break; + } + } + } + + String identitySuffix = "." + writeIdentityIndex; + + // Write name used in identity + String identityName = (identity.name == null) ? "" : identity.name; + putString(editor, accountKeyPrefix + Account.IDENTITY_NAME_KEY + identitySuffix, + identityName); + + // Validate email address + if (!IdentitySettings.isEmailAddressValid(identity.email)) { + throw new InvalidSettingValueException(); + } + + // Write email address + putString(editor, accountKeyPrefix + Account.IDENTITY_EMAIL_KEY + identitySuffix, + identity.email); + + // Write identity description + putString(editor, accountKeyPrefix + Account.IDENTITY_DESCRIPTION_KEY + identitySuffix, + identityDescription); + + if (identity.settings != null) { + // Validate identity settings + Map validatedSettings = IdentitySettings.validate( + identity.settings.settings, !mergeSettings); + + // Merge identity settings if necessary + Map writeSettings; + if (mergeSettings) { + writeSettings = new HashMap(IdentitySettings.getIdentitySettings( + prefs.getPreferences(), uuid, writeIdentityIndex)); + writeSettings.putAll(validatedSettings); + } else { + writeSettings = validatedSettings; + } + + // Write identity settings + for (Map.Entry setting : writeSettings.entrySet()) { + String key = accountKeyPrefix + setting.getKey() + identitySuffix; + String value = setting.getValue(); + putString(editor, key, value); + } + } + } + } + + private static boolean isAccountNameUsed(String name, Account[] accounts) { + for (Account account : accounts) { + if (account.getDescription().equals(name)) { + return true; + } + } + return false; + } + + private static boolean isIdentityDescriptionUsed(String description, List identities) { + for (Identity identitiy : identities) { + if (identitiy.getDescription().equals(description)) { + return true; + } + } + return false; + } + + private static int findIdentity(ImportedIdentity identity, + List identities) { + for (int i = 0; i < identities.size(); i++) { + Identity existingIdentity = identities.get(i); + if (existingIdentity.getName().equals(identity.name) && + existingIdentity.getEmail().equals(identity.email)) { + return i; + } + } + return -1; + } + + /** + * Write to an {@link SharedPreferences.Editor} while logging what is written if debug logging + * is enabled. + * + * @param editor + * The {@code Editor} to write to. + * @param key + * The name of the preference to modify. + * @param value + * The new value for the preference. + */ + private static void putString(SharedPreferences.Editor editor, String key, String value) { + if (K9.DEBUG) { + String outputValue = value; + if (!K9.DEBUG_SENSITIVE && + (key.endsWith(".transportUri") || key.endsWith(".storeUri"))) { + outputValue = "*sensitive*"; + } + Log.v(K9.LOG_TAG, "Setting " + key + "=" + outputValue); + } + editor.putString(key, value); + } + + private static Imported parseSettings(InputStream inputStream, boolean globalSettings, + List accountUuids, boolean overview) + throws SettingsImportExportException { + + if (!overview && accountUuids == null) { + throw new IllegalArgumentException("Argument 'accountUuids' must not be null."); + } + + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + //factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + + InputStreamReader reader = new InputStreamReader(inputStream); + xpp.setInput(reader); + + Imported imported = null; + int eventType = xpp.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if(eventType == XmlPullParser.START_TAG) { + if (SettingsExporter.ROOT_ELEMENT.equals(xpp.getName())) { + imported = parseRoot(xpp, globalSettings, accountUuids, overview); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + if (imported == null || (overview && imported.globalSettings == null && + imported.accounts == null)) { + throw new SettingsImportExportException("Invalid import data"); + } + + return imported; + } catch (Exception e) { + throw new SettingsImportExportException(e); + } + } + + private static void skipToEndTag(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + eventType = xpp.next(); + } + } + + private static String getText(XmlPullParser xpp) + throws XmlPullParserException, IOException { + + int eventType = xpp.next(); + if (eventType != XmlPullParser.TEXT) { + return ""; + } + return xpp.getText(); + } + + private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings, + List accountUuids, boolean overview) + throws XmlPullParserException, IOException, SettingsImportExportException { + + Imported result = new Imported(); + + String fileFormatVersionString = xpp.getAttributeValue(null, + SettingsExporter.FILE_FORMAT_ATTRIBUTE); + validateFileFormatVersion(fileFormatVersionString); + + String contentVersionString = xpp.getAttributeValue(null, + SettingsExporter.VERSION_ATTRIBUTE); + validateContentVersion(contentVersionString); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ROOT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.GLOBAL_ELEMENT.equals(element)) { + if (overview || globalSettings) { + if (result.globalSettings == null) { + if (overview) { + result.globalSettings = new ImportedSettings(); + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + } else { + result.globalSettings = parseSettings(xpp, SettingsExporter.GLOBAL_ELEMENT); + } + } else { + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + Log.w(K9.LOG_TAG, "More than one global settings element. Only using the first one!"); + } + } else { + skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping global settings"); + } + } else if (SettingsExporter.ACCOUNTS_ELEMENT.equals(element)) { + if (result.accounts == null) { + result.accounts = parseAccounts(xpp, accountUuids, overview); + } else { + Log.w(K9.LOG_TAG, "More than one accounts element. Only using the first one!"); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return result; + } + + private static int validateFileFormatVersion(String versionString) + throws SettingsImportExportException { + + if (versionString == null) { + throw new SettingsImportExportException("Missing file format version"); + } + + int version; + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException e) { + throw new SettingsImportExportException("Invalid file format version: " + + versionString); + } + + if (version != SettingsExporter.FILE_FORMAT_VERSION) { + throw new SettingsImportExportException("Unsupported file format version: " + + versionString); + } + + return version; + } + + private static int validateContentVersion(String versionString) + throws SettingsImportExportException { + + if (versionString == null) { + throw new SettingsImportExportException("Missing content version"); + } + + int version; + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException e) { + throw new SettingsImportExportException("Invalid content version: " + + versionString); + } + + if (version != Settings.VERSION) { + throw new SettingsImportExportException("Unsupported content version: " + + versionString); + } + + return version; + } + + private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + + ImportedSettings result = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.VALUE_ELEMENT.equals(element)) { + String key = xpp.getAttributeValue(null, SettingsExporter.KEY_ATTRIBUTE); + String value = getText(xpp); + + if (result == null) { + result = new ImportedSettings(); + } + + if (result.settings.containsKey(key)) { + Log.w(K9.LOG_TAG, "Already read key \"" + key + "\". Ignoring value \"" + value + "\""); + } else { + result.settings.put(key, value); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return result; + } + + private static Map parseAccounts(XmlPullParser xpp, + List accountUuids, boolean overview) + throws XmlPullParserException, IOException { + + Map accounts = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ACCOUNTS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.ACCOUNT_ELEMENT.equals(element)) { + if (accounts == null) { + accounts = new HashMap(); + } + + ImportedAccount account = parseAccount(xpp, accountUuids, overview); + + if (account == null) { + // Do nothing - parseAccount() already logged a message + } else if (!accounts.containsKey(account.uuid)) { + accounts.put(account.uuid, account); + } else { + Log.w(K9.LOG_TAG, "Duplicate account entries with UUID " + account.uuid + + ". Ignoring!"); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return accounts; + } + + private static ImportedAccount parseAccount(XmlPullParser xpp, List accountUuids, + boolean overview) + throws XmlPullParserException, IOException { + + String uuid = xpp.getAttributeValue(null, SettingsExporter.UUID_ATTRIBUTE); + + try { + UUID.fromString(uuid); + } catch (Exception e) { + skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT); + Log.w(K9.LOG_TAG, "Skipping account with invalid UUID " + uuid); + return null; + } + + ImportedAccount account = new ImportedAccount(); + account.uuid = uuid; + + if (overview || accountUuids.contains(uuid)) { + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.ACCOUNT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.NAME_ELEMENT.equals(element)) { + account.name = getText(xpp); + } else if (SettingsExporter.INCOMING_SERVER_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT); + } else { + account.incoming = parseServerSettings(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT); + } + } else if (SettingsExporter.OUTGOING_SERVER_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT); + } else { + account.outgoing = parseServerSettings(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT); + } + } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.SETTINGS_ELEMENT); + } else { + account.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT); + } + } else if (SettingsExporter.IDENTITIES_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.IDENTITIES_ELEMENT); + } else { + account.identities = parseIdentities(xpp); + } + } else if (SettingsExporter.FOLDERS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, SettingsExporter.FOLDERS_ELEMENT); + } else { + account.folders = parseFolders(xpp); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + } else { + skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping account with UUID " + uuid); + } + + return account; + } + + private static ImportedServer parseServerSettings(XmlPullParser xpp, String endTag) + throws XmlPullParserException, IOException { + ImportedServer server = new ImportedServer(); + + server.type = xpp.getAttributeValue(null, SettingsExporter.TYPE_ATTRIBUTE); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) { + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.HOST_ELEMENT.equals(element)) { + server.host = getText(xpp); + } else if (SettingsExporter.PORT_ELEMENT.equals(element)) { + server.port = getText(xpp); + } else if (SettingsExporter.CONNECTION_SECURITY_ELEMENT.equals(element)) { + server.connectionSecurity = getText(xpp); + } else if (SettingsExporter.AUTHENTICATION_TYPE_ELEMENT.equals(element)) { + server.authenticationType = getText(xpp); + } else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) { + server.username = getText(xpp); + } else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) { + server.password = getText(xpp); + } else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) { + server.extras = parseSettings(xpp, SettingsExporter.EXTRA_ELEMENT); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return server; + } + + private static List parseIdentities(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List identities = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.IDENTITIES_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.IDENTITY_ELEMENT.equals(element)) { + if (identities == null) { + identities = new ArrayList(); + } + + ImportedIdentity identity = parseIdentity(xpp); + identities.add(identity); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return identities; + } + + private static ImportedIdentity parseIdentity(XmlPullParser xpp) + throws XmlPullParserException, IOException { + ImportedIdentity identity = new ImportedIdentity(); + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.IDENTITY_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.NAME_ELEMENT.equals(element)) { + identity.name = getText(xpp); + } else if (SettingsExporter.EMAIL_ELEMENT.equals(element)) { + identity.email = getText(xpp); + } else if (SettingsExporter.DESCRIPTION_ELEMENT.equals(element)) { + identity.description = getText(xpp); + } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) { + identity.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return identity; + } + + private static List parseFolders(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List folders = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + SettingsExporter.FOLDERS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (SettingsExporter.FOLDER_ELEMENT.equals(element)) { + if (folders == null) { + folders = new ArrayList(); + } + + ImportedFolder folder = parseFolder(xpp); + folders.add(folder); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return folders; + } + + private static ImportedFolder parseFolder(XmlPullParser xpp) + throws XmlPullParserException, IOException { + ImportedFolder folder = new ImportedFolder(); + + String name = xpp.getAttributeValue(null, SettingsExporter.NAME_ATTRIBUTE); + folder.name = name; + + folder.settings = parseSettings(xpp, SettingsExporter.FOLDER_ELEMENT); + + return folder; + } + + private static class ImportedServerSettings extends ServerSettings { + private final ImportedServer mImportedServer; + + public ImportedServerSettings(ImportedServer server) { + super(server.type, server.host, convertPort(server.port), + convertConnectionSecurity(server.connectionSecurity), + server.authenticationType, server.username, server.password); + mImportedServer = server; + } + + @Override + public Map getExtra() { + return (mImportedServer.extras != null) ? + Collections.unmodifiableMap(mImportedServer.extras.settings) : null; + } + + private static int convertPort(String port) { + try { + return Integer.parseInt(port); + } catch (NumberFormatException e) { + return -1; + } + } + + private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) { + try { + return ConnectionSecurity.valueOf(connectionSecurity); + } catch (Exception e) { + return ConnectionSecurity.NONE; + } + } + } + + private static class Imported { + public ImportedSettings globalSettings; + public Map accounts; + } + + private static class ImportedSettings { + public Map settings = new HashMap(); + } + + private static class ImportedAccount { + public String uuid; + public String name; + public ImportedServer incoming; + public ImportedServer outgoing; + public ImportedSettings settings; + public List identities; + public List folders; + } + + private static class ImportedServer { + public String type; + public String host; + public String port; + public String connectionSecurity; + public String authenticationType; + public String username; + public String password; + public ImportedSettings extras; + } + + private static class ImportedIdentity { + public String name; + public String email; + public String description; + public ImportedSettings settings; + } + + private static class ImportedFolder { + public String name; + public ImportedSettings settings; + } +} diff --git a/src/com/fsck/k9/preferences/TimePickerPreference.java b/src/com/fsck/k9/preferences/TimePickerPreference.java index f829cebfc..a2d6592b3 100644 --- a/src/com/fsck/k9/preferences/TimePickerPreference.java +++ b/src/com/fsck/k9/preferences/TimePickerPreference.java @@ -20,7 +20,7 @@ public class TimePickerPreference extends DialogPreference implements /** * The validation expression for this preference */ - private static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]"; + public static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]"; /** * The default value for this preference diff --git a/src/com/fsck/k9/service/MailService.java b/src/com/fsck/k9/service/MailService.java index 55a668ef1..8f3330073 100644 --- a/src/com/fsck/k9/service/MailService.java +++ b/src/com/fsck/k9/service/MailService.java @@ -274,7 +274,7 @@ public class MailService extends CoreService { } int shortestInterval = -1; - for (Account account : prefs.getAccounts()) { + for (Account account : prefs.getAvailableAccounts()) { if (account.getAutomaticCheckIntervalMinutes() != -1 && account.getFolderSyncMode() != FolderMode.NONE && (account.getAutomaticCheckIntervalMinutes() < shortestInterval || @@ -363,7 +363,7 @@ public class MailService extends CoreService { for (Account account : Preferences.getPreferences(MailService.this).getAccounts()) { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Setting up pushers for account " + account.getDescription()); - if (account.isAvailable(getApplicationContext())) { + if (account.isEnabled() && account.isAvailable(getApplicationContext())) { pushing |= MessagingController.getInstance(getApplication()).setupPushing(account); } else { //TODO: setupPushing of unavailable accounts when they become available (sd-card inserted)