Merge pull request #7766 from thunderbird/settings_import_refactor2
Refactor `SettingsImporter` [2/x]
This commit is contained in:
commit
041330ae1f
8 changed files with 653 additions and 666 deletions
|
@ -1,23 +1,22 @@
|
|||
package com.fsck.k9.preferences
|
||||
|
||||
data class ImportResults(
|
||||
@JvmField val globalSettings: Boolean,
|
||||
@JvmField val importedAccounts: List<AccountDescriptionPair>,
|
||||
@JvmField val erroneousAccounts: List<AccountDescription>,
|
||||
val globalSettings: Boolean,
|
||||
val importedAccounts: List<AccountDescriptionPair>,
|
||||
val erroneousAccounts: List<AccountDescription>,
|
||||
)
|
||||
|
||||
data class AccountDescriptionPair(
|
||||
@JvmField val original: AccountDescription,
|
||||
@JvmField val imported: AccountDescription,
|
||||
@JvmField val overwritten: Boolean,
|
||||
@JvmField val authorizationNeeded: Boolean,
|
||||
@JvmField val incomingPasswordNeeded: Boolean,
|
||||
@JvmField val outgoingPasswordNeeded: Boolean,
|
||||
@JvmField val incomingServerName: String,
|
||||
@JvmField val outgoingServerName: String,
|
||||
val original: AccountDescription,
|
||||
val imported: AccountDescription,
|
||||
val authorizationNeeded: Boolean,
|
||||
val incomingPasswordNeeded: Boolean,
|
||||
val outgoingPasswordNeeded: Boolean,
|
||||
val incomingServerName: String,
|
||||
val outgoingServerName: String,
|
||||
)
|
||||
|
||||
data class AccountDescription(
|
||||
@JvmField val name: String,
|
||||
@JvmField val uuid: String,
|
||||
val name: String,
|
||||
val uuid: String,
|
||||
)
|
||||
|
|
|
@ -23,4 +23,17 @@ val preferencesModule = module {
|
|||
coroutineScope = get(named("AppCoroutineScope")),
|
||||
)
|
||||
} bind GeneralSettingsManager::class
|
||||
|
||||
factory { SettingsFileParser() }
|
||||
factory {
|
||||
SettingsImporter(
|
||||
settingsFileParser = get(),
|
||||
preferences = get(),
|
||||
generalSettingsManager = get(),
|
||||
localFoldersCreator = get(),
|
||||
serverSettingsSerializer = get(),
|
||||
clock = get(),
|
||||
context = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,45 +3,45 @@ package com.fsck.k9.preferences
|
|||
import com.fsck.k9.mail.AuthType
|
||||
|
||||
internal data class Imported(
|
||||
@JvmField val contentVersion: Int,
|
||||
@JvmField val globalSettings: ImportedSettings?,
|
||||
@JvmField val accounts: Map<String, ImportedAccount>?,
|
||||
val contentVersion: Int,
|
||||
val globalSettings: ImportedSettings?,
|
||||
val accounts: Map<String, ImportedAccount>?,
|
||||
)
|
||||
|
||||
internal data class ImportedSettings(
|
||||
@JvmField val settings: Map<String, String> = emptyMap(),
|
||||
val settings: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
internal data class ImportedAccount(
|
||||
@JvmField val uuid: String,
|
||||
@JvmField val name: String?,
|
||||
@JvmField val incoming: ImportedServer?,
|
||||
@JvmField val outgoing: ImportedServer?,
|
||||
@JvmField val settings: ImportedSettings?,
|
||||
@JvmField val identities: List<ImportedIdentity>?,
|
||||
@JvmField val folders: List<ImportedFolder>?,
|
||||
val uuid: String,
|
||||
val name: String?,
|
||||
val incoming: ImportedServer?,
|
||||
val outgoing: ImportedServer?,
|
||||
val settings: ImportedSettings?,
|
||||
val identities: List<ImportedIdentity>?,
|
||||
val folders: List<ImportedFolder>?,
|
||||
)
|
||||
|
||||
internal data class ImportedServer(
|
||||
@JvmField val type: String?,
|
||||
@JvmField val host: String?,
|
||||
@JvmField val port: String?,
|
||||
@JvmField val connectionSecurity: String?,
|
||||
@JvmField val authenticationType: AuthType?,
|
||||
@JvmField val username: String?,
|
||||
@JvmField val password: String?,
|
||||
@JvmField val clientCertificateAlias: String?,
|
||||
@JvmField val extras: ImportedSettings?,
|
||||
val type: String?,
|
||||
val host: String?,
|
||||
val port: String?,
|
||||
val connectionSecurity: String?,
|
||||
val authenticationType: AuthType?,
|
||||
val username: String?,
|
||||
val password: String?,
|
||||
val clientCertificateAlias: String?,
|
||||
val extras: ImportedSettings?,
|
||||
)
|
||||
|
||||
internal data class ImportedIdentity(
|
||||
@JvmField val name: String?,
|
||||
@JvmField val email: String?,
|
||||
@JvmField val description: String?,
|
||||
@JvmField val settings: ImportedSettings?,
|
||||
val name: String?,
|
||||
val email: String?,
|
||||
val description: String?,
|
||||
val settings: ImportedSettings?,
|
||||
)
|
||||
|
||||
internal data class ImportedFolder(
|
||||
@JvmField val name: String?,
|
||||
@JvmField val settings: ImportedSettings?,
|
||||
val name: String?,
|
||||
val settings: ImportedSettings?,
|
||||
)
|
||||
|
|
|
@ -1,612 +0,0 @@
|
|||
package com.fsck.k9.preferences;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.AccountPreferenceSerializer;
|
||||
import com.fsck.k9.Core;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.Identity;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.ServerSettingsSerializer;
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator;
|
||||
import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
|
||||
import kotlinx.datetime.Clock;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
|
||||
public class SettingsImporter {
|
||||
/**
|
||||
* 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)
|
||||
SettingsFileParser settingsFileParser = new SettingsFileParser();
|
||||
Imported imported = settingsFileParser.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<AccountDescription> 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()) {
|
||||
String accountName = getAccountDisplayName(account);
|
||||
accounts.add(new AccountDescription(accountName, 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.<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.
|
||||
*/
|
||||
public static ImportResults importSettings(Context context, InputStream inputStream, boolean globalSettings,
|
||||
List<String> accountUuids, boolean overwrite) throws SettingsImportExportException {
|
||||
|
||||
try {
|
||||
boolean globalSettingsImported = false;
|
||||
List<AccountDescriptionPair> importedAccounts = new ArrayList<>();
|
||||
List<AccountDescription> erroneousAccounts = new ArrayList<>();
|
||||
|
||||
SettingsFileParser settingsFileParser = new SettingsFileParser();
|
||||
Imported imported = settingsFileParser.parseSettings(inputStream, globalSettings, accountUuids, false);
|
||||
|
||||
Preferences preferences = Preferences.getPreferences();
|
||||
Storage storage = preferences.getStorage();
|
||||
|
||||
if (globalSettings) {
|
||||
try {
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
if (imported.globalSettings != null) {
|
||||
importGlobalSettings(storage, editor, imported.contentVersion, imported.globalSettings);
|
||||
} else {
|
||||
Timber.w("Was asked to import global settings but none found.");
|
||||
}
|
||||
if (editor.commit()) {
|
||||
Timber.v("Committed global settings to the preference storage.");
|
||||
globalSettingsImported = true;
|
||||
} else {
|
||||
Timber.v("Failed to commit global settings to the preference storage");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Exception while importing global settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (accountUuids != null && accountUuids.size() > 0) {
|
||||
if (imported.accounts != null) {
|
||||
for (String accountUuid : accountUuids) {
|
||||
if (imported.accounts.containsKey(accountUuid)) {
|
||||
ImportedAccount account = imported.accounts.get(accountUuid);
|
||||
try {
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
|
||||
AccountDescriptionPair importResult = importAccount(context, editor,
|
||||
imported.contentVersion, account, overwrite);
|
||||
|
||||
if (editor.commit()) {
|
||||
Timber.v("Committed settings for account \"%s\" to the settings database.",
|
||||
importResult.imported.name);
|
||||
|
||||
// Add UUID of the account we just imported to the list of
|
||||
// account UUIDs
|
||||
if (!importResult.overwritten) {
|
||||
editor = preferences.createStorageEditor();
|
||||
|
||||
String newUuid = importResult.imported.uuid;
|
||||
String oldAccountUuids = preferences.getStorage().getString("accountUuids", "");
|
||||
String newAccountUuids = (oldAccountUuids.length() > 0) ?
|
||||
oldAccountUuids + "," + newUuid : newUuid;
|
||||
|
||||
putString(editor, "accountUuids", newAccountUuids);
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw new SettingsImportExportException("Failed to set account UUID list");
|
||||
}
|
||||
}
|
||||
|
||||
// Reload accounts
|
||||
preferences.loadAccounts();
|
||||
|
||||
importedAccounts.add(importResult);
|
||||
} else {
|
||||
Timber.w("Error while committing settings for account \"%s\" to the settings " +
|
||||
"database.", importResult.original.name);
|
||||
|
||||
erroneousAccounts.add(importResult.original);
|
||||
}
|
||||
} catch (InvalidSettingValueException e) {
|
||||
String reason = e.getMessage();
|
||||
if (TextUtils.isEmpty(reason)) {
|
||||
reason = "Unknown";
|
||||
}
|
||||
Timber.e(e, "Encountered invalid setting while importing account \"%s\", reason: \"%s\"",
|
||||
account.name, reason);
|
||||
|
||||
erroneousAccounts.add(new AccountDescription(account.name, account.uuid));
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Exception while importing account \"%s\"", account.name);
|
||||
erroneousAccounts.add(new AccountDescription(account.name, account.uuid));
|
||||
}
|
||||
} else {
|
||||
Timber.w("Was asked to import account with UUID %s. But this account wasn't found.",
|
||||
accountUuid);
|
||||
}
|
||||
}
|
||||
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw new SettingsImportExportException("Failed to set default account");
|
||||
}
|
||||
} else {
|
||||
Timber.w("Was asked to import at least one account but none found.");
|
||||
}
|
||||
}
|
||||
|
||||
preferences.loadAccounts();
|
||||
|
||||
SpecialLocalFoldersCreator localFoldersCreator = DI.get(SpecialLocalFoldersCreator.class);
|
||||
|
||||
// Create special local folders
|
||||
for (AccountDescriptionPair importedAccount : importedAccounts) {
|
||||
String accountUuid = importedAccount.imported.uuid;
|
||||
Account account = preferences.getAccount(accountUuid);
|
||||
|
||||
localFoldersCreator.createSpecialLocalFolders(account);
|
||||
}
|
||||
|
||||
DI.get(RealGeneralSettingsManager.class).loadSettings();
|
||||
Core.setServicesEnabled(context);
|
||||
|
||||
return new ImportResults(globalSettingsImported, importedAccounts, erroneousAccounts);
|
||||
|
||||
} catch (SettingsImportExportException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new SettingsImportExportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void importGlobalSettings(Storage storage, StorageEditor editor, int contentVersion,
|
||||
ImportedSettings settings) {
|
||||
|
||||
// Validate global settings
|
||||
Map<String, Object> validatedSettings = GeneralSettingsDescriptions.validate(contentVersion, settings.settings);
|
||||
|
||||
// Upgrade global settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
GeneralSettingsDescriptions.upgrade(contentVersion, validatedSettings);
|
||||
}
|
||||
|
||||
// Convert global settings to the string representation used in preference storage
|
||||
Map<String, String> stringSettings = GeneralSettingsDescriptions.convert(validatedSettings);
|
||||
|
||||
// Use current global settings as base and overwrite with validated settings read from the import file.
|
||||
Map<String, String> mergedSettings = new HashMap<>(GeneralSettingsDescriptions.getGlobalSettings(storage));
|
||||
mergedSettings.putAll(stringSettings);
|
||||
|
||||
for (Map.Entry<String, String> setting : mergedSettings.entrySet()) {
|
||||
String key = setting.getKey();
|
||||
String value = setting.getValue();
|
||||
putString(editor, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static AccountDescriptionPair importAccount(Context context, StorageEditor editor, int contentVersion,
|
||||
ImportedAccount account, boolean overwrite) throws InvalidSettingValueException {
|
||||
|
||||
AccountDescription original = new AccountDescription(account.name, account.uuid);
|
||||
|
||||
Preferences prefs = Preferences.getPreferences();
|
||||
List<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;
|
||||
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.size(); i++) {
|
||||
accountName = account.name + " (" + i + ")";
|
||||
if (!isAccountNameUsed(accountName, accounts)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write account name
|
||||
String accountKeyPrefix = uuid + ".";
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.ACCOUNT_DESCRIPTION_KEY, accountName);
|
||||
|
||||
if (account.incoming == null) {
|
||||
// We don't import accounts without incoming server settings
|
||||
throw new InvalidSettingValueException("Missing incoming server settings");
|
||||
}
|
||||
|
||||
// Write incoming server settings
|
||||
ServerSettings incoming = createServerSettings(account.incoming);
|
||||
ServerSettingsSerializer serverSettingsSerializer = DI.get(ServerSettingsSerializer.class);
|
||||
String incomingServer = serverSettingsSerializer.serialize(incoming);
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY, incomingServer);
|
||||
|
||||
String incomingServerName = incoming.host;
|
||||
boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType &&
|
||||
AuthType.XOAUTH2 != incoming.authenticationType &&
|
||||
(incoming.password == null || incoming.password.isEmpty());
|
||||
|
||||
boolean authorizationNeeded = incoming.authenticationType == AuthType.XOAUTH2;
|
||||
|
||||
if (account.outgoing == null) {
|
||||
throw new InvalidSettingValueException("Missing outgoing server settings");
|
||||
}
|
||||
|
||||
String outgoingServerName = null;
|
||||
boolean outgoingPasswordNeeded = false;
|
||||
// Write outgoing server settings
|
||||
ServerSettings outgoing = createServerSettings(account.outgoing);
|
||||
String outgoingServer = serverSettingsSerializer.serialize(outgoing);
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY, outgoingServer);
|
||||
|
||||
/*
|
||||
* Mark account as disabled if the settings file contained a username but no password, except when the
|
||||
* AuthType is EXTERNAL.
|
||||
*/
|
||||
outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType &&
|
||||
AuthType.XOAUTH2 != outgoing.authenticationType &&
|
||||
outgoing.username != null &&
|
||||
!outgoing.username.isEmpty() &&
|
||||
(outgoing.password == null || outgoing.password.isEmpty());
|
||||
|
||||
authorizationNeeded |= outgoing.authenticationType == AuthType.XOAUTH2;
|
||||
|
||||
outgoingServerName = outgoing.host;
|
||||
|
||||
boolean createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded || authorizationNeeded;
|
||||
if (createAccountDisabled) {
|
||||
editor.putBoolean(accountKeyPrefix + "enabled", false);
|
||||
}
|
||||
|
||||
// Validate account settings
|
||||
Map<String, Object> validatedSettings =
|
||||
AccountSettingsDescriptions.validate(contentVersion, account.settings.settings, !mergeImportedAccount);
|
||||
|
||||
// Upgrade account settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
AccountSettingsDescriptions.upgrade(contentVersion, validatedSettings);
|
||||
}
|
||||
|
||||
// Convert account settings to the string representation used in preference storage
|
||||
Map<String, String> stringSettings = AccountSettingsDescriptions.convert(validatedSettings);
|
||||
|
||||
// Merge account settings if necessary
|
||||
Map<String, String> writeSettings;
|
||||
if (mergeImportedAccount) {
|
||||
writeSettings = new HashMap<>(AccountSettingsDescriptions.getAccountSettings(prefs.getStorage(), uuid));
|
||||
writeSettings.putAll(stringSettings);
|
||||
} else {
|
||||
writeSettings = stringSettings;
|
||||
}
|
||||
|
||||
// Write account settings
|
||||
for (Map.Entry<String, String> 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 = prefs.generateAccountNumber();
|
||||
putString(editor, accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber));
|
||||
}
|
||||
|
||||
// Write identities
|
||||
if (account.identities != null) {
|
||||
importIdentities(editor, contentVersion, uuid, account, overwrite, existingAccount, prefs);
|
||||
} else if (!mergeImportedAccount) {
|
||||
// Require accounts to at least have one identity
|
||||
throw new InvalidSettingValueException("Missing identities, there should be at least one.");
|
||||
}
|
||||
|
||||
// Write folder settings
|
||||
if (account.folders != null) {
|
||||
for (ImportedFolder folder : account.folders) {
|
||||
importFolder(editor, contentVersion, uuid, folder, mergeImportedAccount, prefs);
|
||||
}
|
||||
}
|
||||
|
||||
// When deleting an account and then restoring it using settings import, the same account UUID will be used.
|
||||
// To avoid reusing a previously existing notification channel ID, we need to make sure to use a unique value
|
||||
// for `messagesNotificationChannelVersion`.
|
||||
Clock clock = DI.get(Clock.class);
|
||||
String messageNotificationChannelVersion = Long.toString(clock.now().getEpochSeconds());
|
||||
putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion);
|
||||
|
||||
AccountDescription imported = new AccountDescription(accountName, uuid);
|
||||
return new AccountDescriptionPair(original, imported, mergeImportedAccount, authorizationNeeded,
|
||||
incomingPasswordNeeded, outgoingPasswordNeeded, incomingServerName, outgoingServerName);
|
||||
}
|
||||
|
||||
private static void importFolder(StorageEditor editor, int contentVersion, String uuid, ImportedFolder folder,
|
||||
boolean overwrite, Preferences prefs) {
|
||||
|
||||
// Validate folder settings
|
||||
Map<String, Object> validatedSettings =
|
||||
FolderSettingsDescriptions.validate(contentVersion, folder.settings.settings, !overwrite);
|
||||
|
||||
// Upgrade folder settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
FolderSettingsDescriptions.upgrade(contentVersion, validatedSettings);
|
||||
}
|
||||
|
||||
// Convert folder settings to the string representation used in preference storage
|
||||
Map<String, String> stringSettings = FolderSettingsDescriptions.convert(validatedSettings);
|
||||
|
||||
// Merge folder settings if necessary
|
||||
Map<String, String> writeSettings;
|
||||
if (overwrite) {
|
||||
writeSettings = FolderSettingsDescriptions.getFolderSettings(prefs.getStorage(), uuid, folder.name);
|
||||
writeSettings.putAll(stringSettings);
|
||||
} else {
|
||||
writeSettings = stringSettings;
|
||||
}
|
||||
|
||||
// Write folder settings
|
||||
String prefix = uuid + "." + folder.name + ".";
|
||||
for (Map.Entry<String, String> setting : writeSettings.entrySet()) {
|
||||
String key = prefix + setting.getKey();
|
||||
String value = setting.getValue();
|
||||
putString(editor, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void importIdentities(StorageEditor editor, int contentVersion, 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<Identity> 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 identitySuffix = "." + writeIdentityIndex;
|
||||
|
||||
// Write name used in identity
|
||||
String identityName = (identity.name == null) ? "" : identity.name;
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_NAME_KEY + identitySuffix, identityName);
|
||||
|
||||
// Validate email address
|
||||
if (!IdentitySettingsDescriptions.isEmailAddressValid(identity.email)) {
|
||||
throw new InvalidSettingValueException("Invalid email address: " + identity.email);
|
||||
}
|
||||
|
||||
// Write email address
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_EMAIL_KEY + identitySuffix, identity.email);
|
||||
|
||||
// Write identity description
|
||||
if (identity.description != null) {
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_DESCRIPTION_KEY + identitySuffix,
|
||||
identity.description);
|
||||
}
|
||||
|
||||
if (identity.settings != null) {
|
||||
// Validate identity settings
|
||||
Map<String, Object> validatedSettings = IdentitySettingsDescriptions.validate(
|
||||
contentVersion, identity.settings.settings, !mergeSettings);
|
||||
|
||||
// Upgrade identity settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
IdentitySettingsDescriptions.upgrade(contentVersion, validatedSettings);
|
||||
}
|
||||
|
||||
// Convert identity settings to the representation used in preference storage
|
||||
Map<String, String> stringSettings = IdentitySettingsDescriptions.convert(validatedSettings);
|
||||
|
||||
// Merge identity settings if necessary
|
||||
Map<String, String> writeSettings;
|
||||
if (mergeSettings) {
|
||||
writeSettings = new HashMap<>(IdentitySettingsDescriptions.getIdentitySettings(
|
||||
prefs.getStorage(), uuid, writeIdentityIndex));
|
||||
writeSettings.putAll(stringSettings);
|
||||
} else {
|
||||
writeSettings = stringSettings;
|
||||
}
|
||||
|
||||
// Write identity settings
|
||||
for (Map.Entry<String, String> setting : writeSettings.entrySet()) {
|
||||
String key = accountKeyPrefix + setting.getKey() + identitySuffix;
|
||||
String value = setting.getValue();
|
||||
putString(editor, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAccountNameUsed(String name, List<Account> accounts) {
|
||||
for (Account account : accounts) {
|
||||
if (account == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (account.getDisplayName().equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int findIdentity(ImportedIdentity identity, List<Identity> 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(StorageEditor editor, String key, String value) {
|
||||
if (K9.isDebugLoggingEnabled()) {
|
||||
String outputValue = value;
|
||||
if (!K9.isSensitiveDebugLoggingEnabled() &&
|
||||
(key.endsWith("." + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY) ||
|
||||
key.endsWith("." + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY))) {
|
||||
outputValue = "*sensitive*";
|
||||
}
|
||||
Timber.v("Setting %s=%s", key, outputValue);
|
||||
}
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private static String getAccountDisplayName(ImportedAccount account) {
|
||||
String name = account.name;
|
||||
if (TextUtils.isEmpty(name) && account.identities != null && account.identities.size() > 0) {
|
||||
name = account.identities.get(0).email;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static ServerSettings createServerSettings(ImportedServer importedServer) {
|
||||
String type = ServerTypeConverter.toServerSettingsType(importedServer.type);
|
||||
int port = convertPort(importedServer.port);
|
||||
ConnectionSecurity connectionSecurity = convertConnectionSecurity(importedServer.connectionSecurity);
|
||||
String password = importedServer.authenticationType == AuthType.XOAUTH2 ? "" : importedServer.password;
|
||||
Map<String, String> extra = importedServer.extras != null ?
|
||||
unmodifiableMap(importedServer.extras.settings) : emptyMap();
|
||||
|
||||
return new ServerSettings(type, importedServer.host, port, connectionSecurity,
|
||||
importedServer.authenticationType, importedServer.username, password,
|
||||
importedServer.clientCertificateAlias, extra);
|
||||
}
|
||||
|
||||
private static int convertPort(String port) {
|
||||
try {
|
||||
return Integer.parseInt(port);
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) {
|
||||
try {
|
||||
/*
|
||||
* TODO:
|
||||
* Add proper settings validation and upgrade capability for server settings.
|
||||
* Once that exists, move this code into a SettingsUpgrader.
|
||||
*/
|
||||
if ("SSL_TLS_OPTIONAL".equals(connectionSecurity)) {
|
||||
return ConnectionSecurity.SSL_TLS_REQUIRED;
|
||||
} else if ("STARTTLS_OPTIONAL".equals(connectionSecurity)) {
|
||||
return ConnectionSecurity.STARTTLS_REQUIRED;
|
||||
}
|
||||
return ConnectionSecurity.valueOf(connectionSecurity);
|
||||
} catch (Exception e) {
|
||||
return ConnectionSecurity.SSL_TLS_REQUIRED;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,554 @@
|
|||
package com.fsck.k9.preferences
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.AccountPreferenceSerializer
|
||||
import com.fsck.k9.Core
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.ServerSettingsSerializer
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
|
||||
import com.fsck.k9.preferences.ServerTypeConverter.toServerSettingsType
|
||||
import com.fsck.k9.preferences.Settings.InvalidSettingValueException
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
import kotlinx.datetime.Clock
|
||||
import timber.log.Timber
|
||||
|
||||
// TODO: Further refactor this class to be able to get rid of these detekt issues.
|
||||
@Suppress(
|
||||
"LongMethod",
|
||||
"CyclomaticComplexMethod",
|
||||
"NestedBlockDepth",
|
||||
"TooManyFunctions",
|
||||
"TooGenericExceptionCaught",
|
||||
"SwallowedException",
|
||||
"ReturnCount",
|
||||
"ThrowsCount",
|
||||
)
|
||||
class SettingsImporter internal constructor(
|
||||
private val settingsFileParser: SettingsFileParser,
|
||||
private val preferences: Preferences,
|
||||
private val generalSettingsManager: RealGeneralSettingsManager,
|
||||
private val localFoldersCreator: SpecialLocalFoldersCreator,
|
||||
private val serverSettingsSerializer: ServerSettingsSerializer,
|
||||
private val clock: Clock,
|
||||
private val context: Context,
|
||||
) {
|
||||
/**
|
||||
* Parses an import [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 `InputStream` to read the settings from.
|
||||
*
|
||||
* @return An [ImportContents] instance containing information about the contents of the settings file.
|
||||
*
|
||||
* @throws SettingsImportExportException In case of an error.
|
||||
*/
|
||||
@Throws(SettingsImportExportException::class)
|
||||
fun getImportStreamContents(inputStream: InputStream): ImportContents {
|
||||
try {
|
||||
// Parse the import stream but don't save individual settings (overview=true)
|
||||
val imported = settingsFileParser.parseSettings(
|
||||
inputStream = inputStream,
|
||||
globalSettings = false,
|
||||
accountUuids = null,
|
||||
overview = true,
|
||||
)
|
||||
|
||||
// If the stream contains global settings the "globalSettings" member will not be null
|
||||
val globalSettings = (imported.globalSettings != null)
|
||||
|
||||
val accounts = imported.accounts?.values.orEmpty().map { importedAccount ->
|
||||
AccountDescription(
|
||||
name = getAccountDisplayName(importedAccount),
|
||||
uuid = importedAccount.uuid,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: throw exception if neither global settings nor account settings could be found
|
||||
return ImportContents(globalSettings, accounts)
|
||||
} catch (e: SettingsImportExportException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw SettingsImportExportException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an import [InputStream] and imports the global settings and/or account configurations specified by the
|
||||
* arguments.
|
||||
*
|
||||
* @param inputStream The `InputStream` to read the settings from.
|
||||
* @param globalSettings `true` if global settings should be imported from the file.
|
||||
* @param accountUuids A list of UUIDs of the accounts that should be imported.
|
||||
*
|
||||
* @return An [ImportResults] instance containing information about errors and successfully imported accounts.
|
||||
*
|
||||
* @throws SettingsImportExportException In case of an error.
|
||||
*/
|
||||
@Throws(SettingsImportExportException::class)
|
||||
fun importSettings(
|
||||
inputStream: InputStream,
|
||||
globalSettings: Boolean,
|
||||
accountUuids: List<String>?,
|
||||
): ImportResults {
|
||||
try {
|
||||
var globalSettingsImported = false
|
||||
val importedAccounts = mutableListOf<AccountDescriptionPair>()
|
||||
val erroneousAccounts = mutableListOf<AccountDescription>()
|
||||
|
||||
val imported = settingsFileParser.parseSettings(
|
||||
inputStream = inputStream,
|
||||
globalSettings = globalSettings,
|
||||
accountUuids = accountUuids,
|
||||
overview = false,
|
||||
)
|
||||
|
||||
val storage = preferences.storage
|
||||
|
||||
if (globalSettings) {
|
||||
try {
|
||||
val editor = preferences.createStorageEditor()
|
||||
if (imported.globalSettings != null) {
|
||||
importGlobalSettings(storage, editor, imported.contentVersion, imported.globalSettings)
|
||||
} else {
|
||||
Timber.w("Was asked to import global settings but none found.")
|
||||
}
|
||||
|
||||
if (editor.commit()) {
|
||||
Timber.v("Committed global settings to the preference storage.")
|
||||
globalSettingsImported = true
|
||||
} else {
|
||||
Timber.v("Failed to commit global settings to the preference storage")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Exception while importing global settings")
|
||||
}
|
||||
}
|
||||
|
||||
if (!accountUuids.isNullOrEmpty()) {
|
||||
if (imported.accounts != null) {
|
||||
for (accountUuid in accountUuids) {
|
||||
val account = imported.accounts[accountUuid]
|
||||
if (account != null) {
|
||||
try {
|
||||
var editor = preferences.createStorageEditor()
|
||||
|
||||
val importResult = importAccount(editor, imported.contentVersion, account)
|
||||
|
||||
if (editor.commit()) {
|
||||
Timber.v(
|
||||
"Committed settings for account \"%s\" to the settings database.",
|
||||
importResult.imported.name,
|
||||
)
|
||||
|
||||
// Add UUID of the account we just imported to the list of account UUIDs
|
||||
editor = preferences.createStorageEditor()
|
||||
|
||||
val newUuid = importResult.imported.uuid
|
||||
val oldAccountUuids = preferences.storage.getString("accountUuids", "")
|
||||
val newAccountUuids = if (oldAccountUuids.isNotEmpty()) {
|
||||
"$oldAccountUuids,$newUuid"
|
||||
} else {
|
||||
newUuid
|
||||
}
|
||||
|
||||
putString(editor, "accountUuids", newAccountUuids)
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw SettingsImportExportException("Failed to set account UUID list")
|
||||
}
|
||||
|
||||
// Reload accounts
|
||||
preferences.loadAccounts()
|
||||
|
||||
importedAccounts.add(importResult)
|
||||
} else {
|
||||
Timber.w(
|
||||
"Error while committing settings for account \"%s\" to the settings database.",
|
||||
importResult.original.name,
|
||||
)
|
||||
|
||||
erroneousAccounts.add(importResult.original)
|
||||
}
|
||||
} catch (e: InvalidSettingValueException) {
|
||||
Timber.e(e, "Encountered invalid setting while importing account \"%s\"", account.name)
|
||||
|
||||
erroneousAccounts.add(AccountDescription(account.name!!, account.uuid))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Exception while importing account \"%s\"", account.name)
|
||||
|
||||
erroneousAccounts.add(AccountDescription(account.name!!, account.uuid))
|
||||
}
|
||||
} else {
|
||||
Timber.w(
|
||||
"Was asked to import account with UUID %s. But this account wasn't found.",
|
||||
accountUuid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val editor = preferences.createStorageEditor()
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw SettingsImportExportException("Failed to set default account")
|
||||
}
|
||||
} else {
|
||||
Timber.w("Was asked to import at least one account but none found.")
|
||||
}
|
||||
}
|
||||
|
||||
preferences.loadAccounts()
|
||||
|
||||
// Create special local folders
|
||||
for (importedAccount in importedAccounts) {
|
||||
val accountUuid = importedAccount.imported.uuid
|
||||
val account = preferences.getAccount(accountUuid) ?: error("Failed to load account: $accountUuid")
|
||||
|
||||
localFoldersCreator.createSpecialLocalFolders(account)
|
||||
}
|
||||
|
||||
generalSettingsManager.loadSettings()
|
||||
Core.setServicesEnabled(context)
|
||||
|
||||
return ImportResults(globalSettingsImported, importedAccounts, erroneousAccounts)
|
||||
} catch (e: SettingsImportExportException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw SettingsImportExportException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importGlobalSettings(
|
||||
storage: Storage,
|
||||
editor: StorageEditor,
|
||||
contentVersion: Int,
|
||||
settings: ImportedSettings,
|
||||
) {
|
||||
// Validate global settings
|
||||
val validatedSettings = GeneralSettingsDescriptions.validate(contentVersion, settings.settings)
|
||||
|
||||
// Upgrade global settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
GeneralSettingsDescriptions.upgrade(contentVersion, validatedSettings)
|
||||
}
|
||||
|
||||
// Convert global settings to the string representation used in preference storage
|
||||
val stringSettings = GeneralSettingsDescriptions.convert(validatedSettings)
|
||||
|
||||
// Use current global settings as base and overwrite with validated settings read from the import file.
|
||||
val mergedSettings = GeneralSettingsDescriptions.getGlobalSettings(storage).toMutableMap()
|
||||
mergedSettings.putAll(stringSettings)
|
||||
|
||||
for ((key, value) in mergedSettings) {
|
||||
putString(editor, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(InvalidSettingValueException::class)
|
||||
private fun importAccount(
|
||||
editor: StorageEditor,
|
||||
contentVersion: Int,
|
||||
account: ImportedAccount,
|
||||
): AccountDescriptionPair {
|
||||
val original = AccountDescription(account.name!!, account.uuid)
|
||||
|
||||
val prefs = Preferences.getPreferences()
|
||||
val accounts = prefs.getAccounts()
|
||||
|
||||
val existingAccount = prefs.getAccount(account.uuid)
|
||||
val uuid = if (existingAccount != null) {
|
||||
// An account with this UUID already exists. So generate a new UUID.
|
||||
UUID.randomUUID().toString()
|
||||
} else {
|
||||
account.uuid
|
||||
}
|
||||
|
||||
// Make sure the account name is unique
|
||||
var accountName = account.name
|
||||
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 (i in 1..accounts.size) {
|
||||
accountName = account.name + " (" + i + ")"
|
||||
if (!isAccountNameUsed(accountName, accounts)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write account name
|
||||
val accountKeyPrefix = "$uuid."
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.ACCOUNT_DESCRIPTION_KEY, accountName)
|
||||
|
||||
if (account.incoming == null) {
|
||||
// We don't import accounts without incoming server settings
|
||||
throw InvalidSettingValueException("Missing incoming server settings")
|
||||
}
|
||||
|
||||
// Write incoming server settings
|
||||
val incoming = createServerSettings(account.incoming)
|
||||
val incomingServer = serverSettingsSerializer.serialize(incoming)
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY, incomingServer)
|
||||
|
||||
val incomingServerName = incoming.host
|
||||
val incomingPasswordNeeded =
|
||||
incoming.authenticationType != AuthType.EXTERNAL && incoming.authenticationType != AuthType.XOAUTH2 &&
|
||||
incoming.password.isNullOrEmpty()
|
||||
|
||||
var authorizationNeeded = incoming.authenticationType == AuthType.XOAUTH2
|
||||
|
||||
if (account.outgoing == null) {
|
||||
throw InvalidSettingValueException("Missing outgoing server settings")
|
||||
}
|
||||
|
||||
// Write outgoing server settings
|
||||
val outgoing = createServerSettings(account.outgoing)
|
||||
val outgoingServer = serverSettingsSerializer.serialize(outgoing)
|
||||
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY, outgoingServer)
|
||||
|
||||
/*
|
||||
* Mark account as disabled if the settings file contained a username but no password, except when the
|
||||
* AuthType is EXTERNAL.
|
||||
*/
|
||||
val outgoingPasswordNeeded =
|
||||
outgoing.authenticationType != AuthType.EXTERNAL && outgoing.authenticationType != AuthType.XOAUTH2 &&
|
||||
outgoing.username.isNotEmpty() && outgoing.password.isNullOrEmpty()
|
||||
|
||||
authorizationNeeded = authorizationNeeded || outgoing.authenticationType == AuthType.XOAUTH2
|
||||
|
||||
val outgoingServerName = outgoing.host
|
||||
|
||||
val createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded || authorizationNeeded
|
||||
if (createAccountDisabled) {
|
||||
editor.putBoolean(accountKeyPrefix + "enabled", false)
|
||||
}
|
||||
|
||||
// Validate account settings
|
||||
val validatedSettings =
|
||||
AccountSettingsDescriptions.validate(contentVersion, account.settings!!.settings, true)
|
||||
|
||||
// Upgrade account settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
AccountSettingsDescriptions.upgrade(contentVersion, validatedSettings)
|
||||
}
|
||||
|
||||
// Convert account settings to the string representation used in preference storage
|
||||
val stringSettings = AccountSettingsDescriptions.convert(validatedSettings)
|
||||
|
||||
// Write account settings
|
||||
for ((accountKey, value) in stringSettings) {
|
||||
val key = accountKeyPrefix + accountKey
|
||||
putString(editor, key, value)
|
||||
}
|
||||
|
||||
// Generate and write a new "accountNumber"
|
||||
val newAccountNumber = prefs.generateAccountNumber()
|
||||
putString(editor, accountKeyPrefix + "accountNumber", newAccountNumber.toString())
|
||||
|
||||
// Write identities
|
||||
if (account.identities != null) {
|
||||
importIdentities(editor, contentVersion, uuid, account)
|
||||
} else {
|
||||
// Require accounts to at least have one identity
|
||||
throw InvalidSettingValueException("Missing identities, there should be at least one.")
|
||||
}
|
||||
|
||||
// Write folder settings
|
||||
if (account.folders != null) {
|
||||
for (folder in account.folders) {
|
||||
importFolder(editor, contentVersion, uuid, folder)
|
||||
}
|
||||
}
|
||||
|
||||
// When deleting an account and then restoring it using settings import, the same account UUID will be used.
|
||||
// To avoid reusing a previously existing notification channel ID, we need to make sure to use a unique value
|
||||
// for `messagesNotificationChannelVersion`.
|
||||
val messageNotificationChannelVersion = clock.now().epochSeconds.toString()
|
||||
putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion)
|
||||
|
||||
val imported = AccountDescription(accountName!!, uuid)
|
||||
return AccountDescriptionPair(
|
||||
original,
|
||||
imported,
|
||||
authorizationNeeded,
|
||||
incomingPasswordNeeded,
|
||||
outgoingPasswordNeeded,
|
||||
incomingServerName!!,
|
||||
outgoingServerName!!,
|
||||
)
|
||||
}
|
||||
|
||||
private fun importFolder(
|
||||
editor: StorageEditor,
|
||||
contentVersion: Int,
|
||||
uuid: String,
|
||||
folder: ImportedFolder,
|
||||
) {
|
||||
// Validate folder settings
|
||||
val validatedSettings =
|
||||
FolderSettingsDescriptions.validate(contentVersion, folder.settings!!.settings, true)
|
||||
|
||||
// Upgrade folder settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
FolderSettingsDescriptions.upgrade(contentVersion, validatedSettings)
|
||||
}
|
||||
|
||||
// Convert folder settings to the string representation used in preference storage
|
||||
val stringSettings = FolderSettingsDescriptions.convert(validatedSettings)
|
||||
|
||||
// Write folder settings
|
||||
val prefix = uuid + "." + folder.name + "."
|
||||
for ((folderKey, value) in stringSettings) {
|
||||
val key = prefix + folderKey
|
||||
putString(editor, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(InvalidSettingValueException::class)
|
||||
private fun importIdentities(
|
||||
editor: StorageEditor,
|
||||
contentVersion: Int,
|
||||
uuid: String,
|
||||
account: ImportedAccount,
|
||||
) {
|
||||
val accountKeyPrefix = "$uuid."
|
||||
|
||||
// Gather information about existing identities for this account (if any)
|
||||
|
||||
// Write identities
|
||||
for ((index, identity) in account.identities!!.withIndex()) {
|
||||
val identitySuffix = ".$index"
|
||||
|
||||
// Write name used in identity
|
||||
val identityName = identity.name.orEmpty()
|
||||
putString(
|
||||
editor,
|
||||
accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_NAME_KEY + identitySuffix,
|
||||
identityName,
|
||||
)
|
||||
|
||||
// Validate email address
|
||||
if (!IdentitySettingsDescriptions.isEmailAddressValid(identity.email)) {
|
||||
throw InvalidSettingValueException("Invalid email address: " + identity.email)
|
||||
}
|
||||
|
||||
// Write email address
|
||||
putString(
|
||||
editor,
|
||||
accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_EMAIL_KEY + identitySuffix,
|
||||
identity.email,
|
||||
)
|
||||
|
||||
// Write identity description
|
||||
if (identity.description != null) {
|
||||
putString(
|
||||
editor,
|
||||
accountKeyPrefix + AccountPreferenceSerializer.IDENTITY_DESCRIPTION_KEY + identitySuffix,
|
||||
identity.description,
|
||||
)
|
||||
}
|
||||
|
||||
if (identity.settings != null) {
|
||||
// Validate identity settings
|
||||
val validatedSettings = IdentitySettingsDescriptions.validate(
|
||||
contentVersion,
|
||||
identity.settings.settings,
|
||||
true,
|
||||
)
|
||||
|
||||
// Upgrade identity settings to current content version
|
||||
if (contentVersion != Settings.VERSION) {
|
||||
IdentitySettingsDescriptions.upgrade(contentVersion, validatedSettings)
|
||||
}
|
||||
|
||||
// Convert identity settings to the representation used in preference storage
|
||||
val stringSettings = IdentitySettingsDescriptions.convert(validatedSettings)
|
||||
|
||||
// Write identity settings
|
||||
for ((identityKey, value) in stringSettings) {
|
||||
val key = accountKeyPrefix + identityKey + identitySuffix
|
||||
putString(editor, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAccountNameUsed(name: String?, accounts: List<Account>): Boolean {
|
||||
return accounts.any { it.displayName == name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to a [StorageEditor] while logging what is written if debug logging is enabled.
|
||||
*
|
||||
* @param editor The `Editor` to write to.
|
||||
* @param key The name of the preference to modify.
|
||||
* @param value The new value for the preference.
|
||||
*/
|
||||
private fun putString(editor: StorageEditor, key: String, value: String?) {
|
||||
if (K9.isDebugLoggingEnabled) {
|
||||
var outputValue = value
|
||||
if (!K9.isSensitiveDebugLoggingEnabled &&
|
||||
(
|
||||
key.endsWith("." + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY) ||
|
||||
key.endsWith("." + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY)
|
||||
)
|
||||
) {
|
||||
outputValue = "*sensitive*"
|
||||
}
|
||||
|
||||
Timber.v("Setting %s=%s", key, outputValue)
|
||||
}
|
||||
|
||||
editor.putString(key, value)
|
||||
}
|
||||
|
||||
private fun getAccountDisplayName(account: ImportedAccount): String {
|
||||
return account.name?.takeIf { it.isNotEmpty() }
|
||||
?: account.identities?.firstOrNull()?.email
|
||||
?: error("Account name missing")
|
||||
}
|
||||
|
||||
private fun createServerSettings(importedServer: ImportedServer): ServerSettings {
|
||||
val type = toServerSettingsType(importedServer.type!!)
|
||||
val port = convertPort(importedServer.port)
|
||||
val connectionSecurity = convertConnectionSecurity(importedServer.connectionSecurity)
|
||||
val password = if (importedServer.authenticationType == AuthType.XOAUTH2) "" else importedServer.password
|
||||
val extra = importedServer.extras?.settings.orEmpty()
|
||||
|
||||
return ServerSettings(
|
||||
type,
|
||||
importedServer.host,
|
||||
port,
|
||||
connectionSecurity,
|
||||
importedServer.authenticationType!!,
|
||||
importedServer.username!!,
|
||||
password,
|
||||
importedServer.clientCertificateAlias,
|
||||
extra,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPort(port: String?): Int {
|
||||
return port?.toIntOrNull() ?: -1
|
||||
}
|
||||
|
||||
private fun convertConnectionSecurity(connectionSecurity: String?): ConnectionSecurity {
|
||||
try {
|
||||
// TODO: Add proper settings validation and upgrade capability for server settings. Once that exists, move
|
||||
// this code into a SettingsUpgrader.
|
||||
if ("SSL_TLS_OPTIONAL" == connectionSecurity) {
|
||||
return ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
} else if ("STARTTLS_OPTIONAL" == connectionSecurity) {
|
||||
return ConnectionSecurity.STARTTLS_REQUIRED
|
||||
}
|
||||
return ConnectionSecurity.valueOf(connectionSecurity!!)
|
||||
} catch (e: Exception) {
|
||||
return ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,13 +14,33 @@ import assertk.assertions.isTrue
|
|||
import assertk.assertions.prop
|
||||
import com.fsck.k9.K9RobolectricTest
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.ServerSettingsSerializer
|
||||
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
|
||||
import java.util.UUID
|
||||
import kotlinx.datetime.Clock
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.test.inject
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
class SettingsImporterTest : K9RobolectricTest() {
|
||||
private val context: Context = RuntimeEnvironment.getApplication()
|
||||
private val preferences: Preferences by inject()
|
||||
private val realGeneralSettingsManager: RealGeneralSettingsManager by inject()
|
||||
private val specialLocalFoldersCreator: SpecialLocalFoldersCreator by inject()
|
||||
private val serverSettingsSerializer: ServerSettingsSerializer by inject()
|
||||
private val clock: Clock by inject()
|
||||
|
||||
private val settingsImporter = SettingsImporter(
|
||||
settingsFileParser = SettingsFileParser(),
|
||||
preferences = preferences,
|
||||
generalSettingsManager = realGeneralSettingsManager,
|
||||
localFoldersCreator = specialLocalFoldersCreator,
|
||||
serverSettingsSerializer = serverSettingsSerializer,
|
||||
clock = clock,
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
|
@ -38,7 +58,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -48,7 +68,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -58,7 +78,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -68,7 +88,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -78,7 +98,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -88,7 +108,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -98,7 +118,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
val accountUuids = emptyList<String>()
|
||||
|
||||
assertFailure {
|
||||
SettingsImporter.importSettings(context, inputStream, true, accountUuids, true)
|
||||
settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
}.isInstanceOf<SettingsImportExportException>()
|
||||
}
|
||||
|
||||
|
@ -137,7 +157,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
""".trimIndent().byteInputStream()
|
||||
val accountUuids = listOf(accountUuid)
|
||||
|
||||
val results = SettingsImporter.importSettings(context, inputStream, true, accountUuids, false)
|
||||
val results = settingsImporter.importSettings(inputStream, globalSettings = true, accountUuids)
|
||||
|
||||
assertThat(results).all {
|
||||
prop(ImportResults::erroneousAccounts).isEmpty()
|
||||
|
@ -174,7 +194,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
</k9settings>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val results = SettingsImporter.getImportStreamContents(inputStream)
|
||||
val results = settingsImporter.getImportStreamContents(inputStream)
|
||||
|
||||
assertThat(results).all {
|
||||
prop(ImportContents::globalSettings).isFalse()
|
||||
|
@ -207,7 +227,7 @@ class SettingsImporterTest : K9RobolectricTest() {
|
|||
</k9settings>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
val results = SettingsImporter.getImportStreamContents(inputStream)
|
||||
val results = settingsImporter.getImportStreamContents(inputStream)
|
||||
|
||||
assertThat(results).all {
|
||||
prop(ImportContents::globalSettings).isFalse()
|
||||
|
|
|
@ -6,7 +6,13 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||
import org.koin.dsl.module
|
||||
|
||||
val featureSettingsImportModule = module {
|
||||
viewModel { SettingsImportViewModel(context = get(), accountActivator = get()) }
|
||||
viewModel {
|
||||
SettingsImportViewModel(
|
||||
context = get(),
|
||||
settingsImporter = get(),
|
||||
accountActivator = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AuthViewModel(
|
||||
|
|
|
@ -30,6 +30,7 @@ private typealias AccountNumber = Int
|
|||
|
||||
internal class SettingsImportViewModel(
|
||||
private val context: Context,
|
||||
private val settingsImporter: SettingsImporter,
|
||||
private val accountActivator: AccountActivator,
|
||||
) : ViewModel() {
|
||||
private val uiModelLiveData = MutableLiveData<SettingsImportUiModel>()
|
||||
|
@ -382,14 +383,20 @@ internal class SettingsImportViewModel(
|
|||
}
|
||||
|
||||
private fun readSettings(contentUri: Uri): ImportContents {
|
||||
return context.contentResolver.openInputStream(contentUri).use { inputStream ->
|
||||
SettingsImporter.getImportStreamContents(inputStream)
|
||||
val inputStream = context.contentResolver.openInputStream(contentUri)
|
||||
?: error("Failed to open settings file for reading: $contentUri")
|
||||
|
||||
return inputStream.use {
|
||||
settingsImporter.getImportStreamContents(inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importSettings(contentUri: Uri, generalSettings: Boolean, accounts: List<AccountUuid>): ImportResults {
|
||||
return context.contentResolver.openInputStream(contentUri).use { inputStream ->
|
||||
SettingsImporter.importSettings(context, inputStream, generalSettings, accounts, false)
|
||||
val inputStream = context.contentResolver.openInputStream(contentUri)
|
||||
?: error("Failed to open settings file for reading: $contentUri")
|
||||
|
||||
return inputStream.use {
|
||||
settingsImporter.importSettings(inputStream, generalSettings, accounts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue