Skip to content

Commit

Permalink
Optional password export
Browse files Browse the repository at this point in the history
  • Loading branch information
hannesa2 committed May 6, 2020
1 parent 664e444 commit 8bc7517
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 81 deletions.
39 changes: 22 additions & 17 deletions app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import com.fsck.k9.mailstore.FolderRepositoryManager
import com.fsck.k9.preferences.ServerTypeConverter.fromServerSettingsType
import com.fsck.k9.preferences.Settings.InvalidSettingValueException
import com.fsck.k9.preferences.Settings.SettingsDescription
import org.xmlpull.v1.XmlSerializer
import timber.log.Timber
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import org.xmlpull.v1.XmlSerializer
import timber.log.Timber

class SettingsExporter(
private val contentResolver: ContentResolver,
Expand All @@ -29,18 +29,18 @@ class SettingsExporter(
private val folderRepositoryManager: FolderRepositoryManager
) {
@Throws(SettingsImportExportException::class)
fun exportToUri(includeGlobals: Boolean, accountUuids: Set<String>, uri: Uri) {
fun exportToUri(includeGlobals: Boolean, accountUuids: Set<String>, uri: Uri, withPassword: Boolean) {
try {
contentResolver.openOutputStream(uri)!!.use { outputStream ->
exportPreferences(outputStream, includeGlobals, accountUuids)
exportPreferences(outputStream, includeGlobals, accountUuids, withPassword)
}
} catch (e: Exception) {
throw SettingsImportExportException(e)
}
}

@Throws(SettingsImportExportException::class)
fun exportPreferences(outputStream: OutputStream, includeGlobals: Boolean, accountUuids: Set<String>) {
fun exportPreferences(outputStream: OutputStream, includeGlobals: Boolean, accountUuids: Set<String>, withPassword: Boolean) {
try {
val serializer = Xml.newSerializer()
serializer.setOutput(outputStream, "UTF-8")
Expand Down Expand Up @@ -68,7 +68,7 @@ class SettingsExporter(
serializer.startTag(null, ACCOUNTS_ELEMENT)
for (accountUuid in accountUuids) {
val account = preferences.getAccount(accountUuid)
writeAccount(serializer, account, prefs)
writeAccount(serializer, account, prefs, withPassword)
}
serializer.endTag(null, ACCOUNTS_ELEMENT)

Expand All @@ -90,8 +90,10 @@ class SettingsExporter(
try {
writeKeyAndPrettyValueFromSetting(serializer, key, setting, valueString)
} catch (e: InvalidSettingValueException) {
Timber.w("Global setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", key, valueString)
Timber.w(
"Global setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", key, valueString
)
}
} else {
Timber.d("Couldn't find key \"%s\" in preference storage. Using default value.", key)
Expand All @@ -100,7 +102,7 @@ class SettingsExporter(
}
}

private fun writeAccount(serializer: XmlSerializer, account: Account, prefs: Map<String, Any>) {
private fun writeAccount(serializer: XmlSerializer, account: Account, prefs: Map<String, Any>, withPassword: Boolean) {
val identities = mutableSetOf<Int>()
val accountUuid = account.uuid

Expand Down Expand Up @@ -130,9 +132,9 @@ class SettingsExporter(
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name)
}
writeElement(serializer, USERNAME_ELEMENT, incoming.username)
if (withPassword)
writeElement(serializer, PASSWORD_ELEMENT, incoming.password)
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias)
// XXX For now we don't export the password
// writeElement(serializer, PASSWORD_ELEMENT, incoming.password);

var extras = incoming.extra
if (!extras.isNullOrEmpty()) {
Expand Down Expand Up @@ -160,9 +162,9 @@ class SettingsExporter(
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name)
}
writeElement(serializer, USERNAME_ELEMENT, outgoing.username)
if (withPassword)
writeElement(serializer, PASSWORD_ELEMENT, outgoing.password)
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias)
// XXX For now we don't export the password
// writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);

extras = outgoing.extra
if (!extras.isNullOrEmpty()) {
Expand Down Expand Up @@ -353,8 +355,9 @@ class SettingsExporter(
try {
writeKeyAndPrettyValueFromSetting(serializer, identityKey, setting, valueString)
} catch (e: InvalidSettingValueException) {
Timber.w("Identity setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", identityKey, valueString
Timber.w(
"Identity setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", identityKey, valueString
)
}
}
Expand Down Expand Up @@ -390,8 +393,10 @@ class SettingsExporter(
try {
writeKeyAndPrettyValueFromSetting(serializer, key, setting, value)
} catch (e: InvalidSettingValueException) {
Timber.w("Folder setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", key, value)
Timber.w(
"Folder setting \"%s\" has invalid value \"%s\" in preference storage. " +
"This shouldn't happen!", key, value
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;

import androidx.annotation.VisibleForTesting;

import com.fsck.k9.Account;
import com.fsck.k9.AccountPreferenceSerializer;
import com.fsck.k9.Core;
Expand All @@ -33,9 +23,22 @@
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.preferences.Settings.InvalidSettingValueException;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import timber.log.Timber;


Expand Down Expand Up @@ -83,8 +86,8 @@ public static class AccountDescriptionPair {
public final String outgoingServerName;

private AccountDescriptionPair(AccountDescription original, AccountDescription imported,
boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded,
String incomingServerName, String outgoingServerName) {
boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded,
String incomingServerName, String outgoingServerName) {
this.original = original;
this.imported = imported;
this.overwritten = overwritten;
Expand All @@ -101,7 +104,7 @@ public static class ImportResults {
public final List<AccountDescription> erroneousAccounts;

private ImportResults(boolean globalSettings, List<AccountDescriptionPair> importedAccounts,
List<AccountDescription> erroneousAccounts) {
List<AccountDescription> erroneousAccounts) {
this.globalSettings = globalSettings;
this.importedAccounts = importedAccounts;
this.erroneousAccounts = erroneousAccounts;
Expand All @@ -113,14 +116,10 @@ private ImportResults(boolean globalSettings, List<AccountDescriptionPair> impor
* 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.
*
* @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.
* settings file.
* @throws SettingsImportExportException In case of an error.
*/
public static ImportContents getImportStreamContents(InputStream inputStream)
throws SettingsImportExportException {
Expand Down Expand Up @@ -157,24 +156,17 @@ public static ImportContents getImportStreamContents(InputStream inputStream)
* 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.<br>
* <strong>Note:</strong> 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!
* @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.<br>
* <strong>Note:</strong> 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.
* successfully imported accounts.
* @throws SettingsImportExportException In case of an error.
*/
public static ImportResults importSettings(Context context, InputStream inputStream, boolean globalSettings,
List<String> accountUuids, boolean overwrite) throws SettingsImportExportException {
Expand Down Expand Up @@ -286,8 +278,7 @@ public static ImportResults importSettings(Context context, InputStream inputStr

// create missing OUTBOX folders
for (AccountDescriptionPair importedAccount : importedAccounts) {
String accountUuid = importedAccount.imported.uuid;
Account account = preferences.getAccount(accountUuid);
String accountUuid = importedAccount.imported.uuid;Account account = preferences.getAccount(accountUuid);
LocalStore localStore = localStoreProvider.getInstance(account);

long outboxFolderId = localStore.createLocalFolder(Account.OUTBOX_NAME, FolderType.OUTBOX);
Expand All @@ -309,7 +300,7 @@ public static ImportResults importSettings(Context context, InputStream inputStr
}

private static void importGlobalSettings(Storage storage, StorageEditor editor, int contentVersion,
ImportedSettings settings) {
ImportedSettings settings) {

// Validate global settings
Map<String, Object> validatedSettings = GeneralSettingsDescriptions.validate(contentVersion, settings.settings);
Expand All @@ -334,7 +325,7 @@ private static void importGlobalSettings(Storage storage, StorageEditor editor,
}

private static AccountDescriptionPair importAccount(Context context, StorageEditor editor, int contentVersion,
ImportedAccount account, boolean overwrite) throws InvalidSettingValueException {
ImportedAccount account, boolean overwrite) throws InvalidSettingValueException {

AccountDescription original = new AccountDescription(account.name, account.uuid);

Expand Down Expand Up @@ -399,7 +390,7 @@ private static AccountDescriptionPair importAccount(Context context, StorageEdit

/*
* Mark account as disabled if the settings file contained a username but no password. However, no password
* is required for the outgoing server for WebDAV accounts, because incoming and outgoing servers are
* is required for the outgoing server for WebDAV accounts, because incoming and outgoing servers are
* identical for this account type. Nor is a password required if the AuthType is EXTERNAL.
*/
String outgoingServerType = ServerTypeConverter.toServerSettingsType(outgoing.type);
Expand Down Expand Up @@ -474,7 +465,7 @@ private static AccountDescriptionPair importAccount(Context context, StorageEdit
}

private static void importFolder(StorageEditor editor, int contentVersion, String uuid, ImportedFolder folder,
boolean overwrite, Preferences prefs) {
boolean overwrite, Preferences prefs) {

// Validate folder settings
Map<String, Object> validatedSettings =
Expand Down Expand Up @@ -507,7 +498,7 @@ private static void importFolder(StorageEditor editor, int contentVersion, Strin
}

private static void importIdentities(StorageEditor editor, int contentVersion, String uuid, ImportedAccount account,
boolean overwrite, Account existingAccount, Preferences prefs) throws InvalidSettingValueException {
boolean overwrite, Account existingAccount, Preferences prefs) throws InvalidSettingValueException {

String accountKeyPrefix = uuid + ".";

Expand Down Expand Up @@ -637,12 +628,9 @@ private static int findIdentity(ImportedIdentity identity, List<Identity> identi
* 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.
* @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(StorageEditor editor, String key, String value) {
if (K9.isDebugLoggingEnabled()) {
Expand All @@ -657,7 +645,7 @@ private static void putString(StorageEditor editor, String key, String value) {

@VisibleForTesting
static Imported parseSettings(InputStream inputStream, boolean globalSettings, List<String> accountUuids,
boolean overview) throws SettingsImportExportException {
boolean overview) throws SettingsImportExportException {

if (!overview && accountUuids == null) {
throw new IllegalArgumentException("Argument 'accountUuids' must not be null.");
Expand Down Expand Up @@ -710,7 +698,7 @@ private static String getText(XmlPullParser xpp) throws XmlPullParserException,
}

private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings, List<String> accountUuids,
boolean overview) throws XmlPullParserException, IOException, SettingsImportExportException {
boolean overview) throws XmlPullParserException, IOException, SettingsImportExportException {

Imported result = new Imported();

Expand Down Expand Up @@ -829,7 +817,7 @@ private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag)
}

private static Map<String, ImportedAccount> parseAccounts(XmlPullParser xpp, List<String> accountUuids,
boolean overview) throws XmlPullParserException, IOException {
boolean overview) throws XmlPullParserException, IOException {

Map<String, ImportedAccount> accounts = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class SettingsExporterTest : K9RobolectricTest() {

private fun exportPreferences(globalSettings: Boolean, accounts: Set<String>): Document {
return ByteArrayOutputStream().use { outputStream ->
settingsExporter.exportPreferences(outputStream, globalSettings, accounts)
settingsExporter.exportPreferences(outputStream, globalSettings, accounts, false)
parseXml(outputStream.toByteArray())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class SettingsExportFragment : Fragment() {
val checkBoxItems = items.map { item ->
val checkBoxItem = when (item) {
is SettingsListItem.GeneralSettings -> GeneralSettingsItem()
is SettingsListItem.Passwords -> PasswordItem()
is SettingsListItem.Account -> AccountItem(item)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@ import com.fsck.k9.ui.R
import kotlinx.android.synthetic.main.settings_export_account_list_item.*

private const val GENERAL_SETTINGS_ID = 0L
private const val ACCOUNT_ITEMS_ID_OFFSET = 1L
private const val PASSWORD_ID = 1L
private const val ACCOUNT_ITEMS_ID_OFFSET = 2L


class GeneralSettingsItem : CheckBoxItem(GENERAL_SETTINGS_ID) {
override val type = R.id.settings_export_list_general_item
override val layoutRes = R.layout.settings_export_general_list_item
}

class PasswordItem : CheckBoxItem(PASSWORD_ID) {
override val type = R.id.settings_export_list_password_item
override val layoutRes = R.layout.settings_export_password_item
}

class AccountItem(account: SettingsListItem.Account) : CheckBoxItem(account.accountNumber + ACCOUNT_ITEMS_ID_OFFSET) {
private val displayName = account.displayName
private val email = account.email

override val type = R.id.settings_export_list_account_item
override val layoutRes = R.layout.settings_export_account_list_item

override fun bindView(viewHolder: CheckBoxViewHolder, payloads: MutableList<Any>) {
super.bindView(viewHolder, payloads)
viewHolder.accountDisplayName.text = displayName
viewHolder.accountEmail.text = email
override fun bindView(holder: CheckBoxViewHolder, payloads: MutableList<Any>) {
super.bindView(holder, payloads)
holder.accountDisplayName.text = displayName
holder.accountEmail.text = email
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ class SettingsExportUiModel {
}

sealed class SettingsListItem {
var selected: Boolean = true
var selected: Boolean = true

object GeneralSettings : SettingsListItem()
object Passwords : SettingsListItem()
data class Account(
val accountNumber: Int,
val displayName: String,
Expand Down
Loading

0 comments on commit 8bc7517

Please sign in to comment.