Merge pull request #4769 from k9mail/fix_special_local_folders

Fix special local folders
This commit is contained in:
cketti 2020-05-19 18:15:43 +02:00 committed by GitHub
commit c0b9db4643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 237 additions and 55 deletions

View file

@ -20,12 +20,17 @@ class FolderRepository(
.thenByDescending { it.isInTopGroup }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.folder.name }
fun getFolders(): List<Folder> {
val folders = localStoreProvider.getInstance(account).getPersonalNamespaces(false)
return folders.map { Folder(it.databaseId, it.serverId, it.name, it.type.toFolderType(), it.isLocalOnly) }
}
fun getRemoteFolders(): List<Folder> {
val folders = localStoreProvider.getInstance(account).getPersonalNamespaces(false)
return folders
.filterNot { it.isLocalOnly }
.map { Folder(it.databaseId, it.serverId, it.name, it.type.toFolderType()) }
.map { Folder(it.databaseId, it.serverId, it.name, it.type.toFolderType(), isLocalOnly = false) }
}
fun getDisplayFolders(displayMode: FolderMode?): List<DisplayFolder> {
@ -111,7 +116,8 @@ class FolderRepository(
"poll_class",
"display_class",
"notify_class",
"push_class"
"push_class",
"local_only"
),
selection,
selectionArgs,
@ -126,7 +132,8 @@ class FolderRepository(
id = id,
serverId = cursor.getString(1),
name = cursor.getString(2),
type = folderTypeOf(id)
type = folderTypeOf(id),
isLocalOnly = cursor.getInt(9) == 1
),
isInTopGroup = cursor.getInt(3) == 1,
isIntegrate = cursor.getInt(4) == 1,
@ -157,7 +164,7 @@ class FolderRepository(
private fun getDisplayFolders(db: SQLiteDatabase, displayMode: FolderMode): List<DisplayFolder> {
val queryBuilder = StringBuilder("""
SELECT f.id, f.server_id, f.name, f.top_group, (
SELECT f.id, f.server_id, f.name, f.top_group, f.local_only, (
SELECT COUNT(m.id)
FROM messages m
WHERE m.folder_id = f.id AND m.empty = 0 AND m.deleted = 0 AND m.read = 0
@ -178,9 +185,10 @@ class FolderRepository(
val name = cursor.getString(2)
val type = folderTypeOf(id)
val isInTopGroup = cursor.getInt(3) == 1
val unreadCount = cursor.getInt(4)
val isLocalOnly = cursor.getInt(4) == 1
val unreadCount = cursor.getInt(5)
val folder = Folder(id, serverId, name, type)
val folder = Folder(id, serverId, name, type, isLocalOnly)
displayFolders.add(DisplayFolder(folder, isInTopGroup, unreadCount))
}
@ -263,7 +271,7 @@ class FolderRepository(
}
}
data class Folder(val id: Long, val serverId: String, val name: String, val type: FolderType)
data class Folder(val id: Long, val serverId: String, val name: String, val type: FolderType, val isLocalOnly: Boolean)
data class FolderDetails(
val folder: Folder,

View file

@ -9,4 +9,5 @@ val mailStoreModule = module {
single { SearchStatusManager() }
single { SpecialFolderSelectionStrategy() }
single { K9BackendStorageFactory(get(), get(), get(), get()) }
factory { SpecialLocalFoldersCreator(preferences = get(), localStoreProvider = get()) }
}

View file

@ -16,7 +16,7 @@ class SpecialFolderUpdater(
private val account: Account
) {
fun updateSpecialFolders() {
val folders = folderRepository.getRemoteFolders()
val folders = folderRepository.getFolders()
updateInbox(folders)
updateSpecialFolder(FolderType.ARCHIVE, folders)

View file

@ -0,0 +1,45 @@
package com.fsck.k9.mailstore
import com.fsck.k9.Account
import com.fsck.k9.Account.SpecialFolderSelection
import com.fsck.k9.Preferences
import com.fsck.k9.mail.FolderType
class SpecialLocalFoldersCreator(
private val preferences: Preferences,
private val localStoreProvider: LocalStoreProvider
) {
fun createSpecialLocalFolders(account: Account) {
check(account.outboxFolderId == null) { "Outbox folder was already set up" }
val localStore = localStoreProvider.getInstance(account)
account.outboxFolderId = localStore.createLocalFolder(OUTBOX_FOLDER_NAME, FolderType.OUTBOX)
if (account.isPop3()) {
check(account.draftsFolderId == null) { "Drafts folder was already set up" }
check(account.sentFolderId == null) { "Sent folder was already set up" }
check(account.trashFolderId == null) { "Trash folder was already set up" }
val draftsFolderId = localStore.createLocalFolder(DRAFTS_FOLDER_NAME, FolderType.DRAFTS)
account.setDraftsFolderId(draftsFolderId, SpecialFolderSelection.MANUAL)
val sentFolderId = localStore.createLocalFolder(SENT_FOLDER_NAME, FolderType.SENT)
account.setSentFolderId(sentFolderId, SpecialFolderSelection.MANUAL)
val trashFolderId = localStore.createLocalFolder(TRASH_FOLDER_NAME, FolderType.TRASH)
account.setTrashFolderId(trashFolderId, SpecialFolderSelection.MANUAL)
}
preferences.saveAccount(account)
}
private fun Account.isPop3() = storeUri.startsWith("pop3")
companion object {
private const val OUTBOX_FOLDER_NAME = Account.OUTBOX_NAME
private const val DRAFTS_FOLDER_NAME = "Drafts"
private const val SENT_FOLDER_NAME = "Sent"
private const val TRASH_FOLDER_NAME = "Trash"
}
}

View file

@ -27,11 +27,9 @@ import com.fsck.k9.Preferences;
import com.fsck.k9.backend.BackendManager;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.FolderType;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator;
import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@ -282,18 +280,14 @@ public class SettingsImporter {
preferences.loadAccounts();
LocalStoreProvider localStoreProvider = DI.get(LocalStoreProvider.class);
SpecialLocalFoldersCreator localFoldersCreator = DI.get(SpecialLocalFoldersCreator.class);
// create missing OUTBOX folders
// Create special local folders
for (AccountDescriptionPair importedAccount : importedAccounts) {
String accountUuid = importedAccount.imported.uuid;
Account account = preferences.getAccount(accountUuid);
LocalStore localStore = localStoreProvider.getInstance(account);
long outboxFolderId = localStore.createLocalFolder(Account.OUTBOX_NAME, FolderType.OUTBOX);
account.setOutboxFolderId(outboxFolderId);
preferences.saveAccount(account);
localFoldersCreator.createSpecialLocalFolders(account);
}
K9.loadPrefs(preferences);

View file

@ -140,6 +140,12 @@ class UnreadWidgetDataProviderTest : AppRobolectricTest() {
const val ACCOUNT_UNREAD_COUNT = 2
const val FOLDER_UNREAD_COUNT = 3
const val LOCALIZED_FOLDER_NAME = "Posteingang"
val FOLDER = Folder(id = FOLDER_ID, serverId = "irrelevant", name = "INBOX", type = FolderType.INBOX)
val FOLDER = Folder(
id = FOLDER_ID,
serverId = "irrelevant",
name = "INBOX",
type = FolderType.INBOX,
isLocalOnly = false
)
}
}

View file

@ -12,7 +12,7 @@ import timber.log.Timber;
class StoreSchemaDefinition implements SchemaDefinition {
static final int DB_VERSION = 75;
static final int DB_VERSION = 76;
private final MigrationsHelper migrationsHelper;

View file

@ -0,0 +1,130 @@
package com.fsck.k9.storage.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.fsck.k9.Account
import com.fsck.k9.helper.map
import com.fsck.k9.mailstore.MigrationsHelper
import timber.log.Timber
/**
* Clean up special local folders
*
* In the past local special folders were not always created. For example, when importing settings or when setting up
* an account, but checking the server settings didn't succeed and the user decided to continue anyway.
*
* Clicking "Next" in the incoming server settings screen would check the server settings and, in the case of success,
* create new special local folders even if they already existed. So it's also possible existing installations have
* multiple special local folders of one type.
*
* Here, we clean up local special folders to have exactly one of each type. Messages in additional folders will be
* moved to the folder we keep and then the other folders will be deleted. An exception are messages in old Outbox
* folders. They will be deleted and not be moved to the new/current Outbox folder because this would cause potentially
* very old messages to be sent. The right thing would be to move them to the Drafts folder. But this is much more
* complicated. They'd have to be uploaded if the Drafts folder is not a local folder. It's also not clear what should
* happen if there is no Drafts folder configured.
*/
internal class MigrationTo76(private val db: SQLiteDatabase, private val migrationsHelper: MigrationsHelper) {
fun cleanUpSpecialLocalFolders() {
val account = migrationsHelper.account
Timber.v("Cleaning up Outbox folder")
val outboxFolderId = account.outboxFolderId ?: createFolder("Outbox", "K9MAIL_INTERNAL_OUTBOX", OUTBOX_FOLDER_TYPE)
deleteOtherOutboxFolders(outboxFolderId)
account.outboxFolderId = outboxFolderId
if (account.isPop3()) {
Timber.v("Cleaning up Drafts folder")
val draftsFolderId = account.draftsFolderId ?: createFolder("Drafts", "Drafts", DRAFTS_FOLDER_TYPE)
moveMessages(DRAFTS_FOLDER_TYPE, draftsFolderId)
account.draftsFolderId = draftsFolderId
Timber.v("Cleaning up Sent folder")
val sentFolderId = account.sentFolderId ?: createFolder("Sent", "Sent", SENT_FOLDER_TYPE)
moveMessages(SENT_FOLDER_TYPE, sentFolderId)
account.sentFolderId = sentFolderId
Timber.v("Cleaning up Trash folder")
val trashFolderId = account.trashFolderId ?: createFolder("Trash", "Trash", TRASH_FOLDER_TYPE)
moveMessages(TRASH_FOLDER_TYPE, trashFolderId)
account.trashFolderId = trashFolderId
}
migrationsHelper.saveAccount()
}
private fun createFolder(name: String, serverId: String, type: String): Long {
Timber.v(" Creating new local folder (name=$name, serverId=$serverId, type=$type)…")
val values = ContentValues().apply {
put("name", name)
put("visible_limit", 25)
put("integrate", 0)
put("top_group", 0)
put("poll_class", "NO_CLASS")
put("push_class", "SECOND_CLASS")
put("display_class", "NO_CLASS")
put("server_id", serverId)
put("local_only", 1)
put("type", type)
}
val folderId = db.insert("folders", null, values)
Timber.v(" Created folder with ID $folderId")
return folderId
}
private fun deleteOtherOutboxFolders(outboxFolderId: Long) {
val otherFolderIds = getOtherFolders(OUTBOX_FOLDER_TYPE, outboxFolderId)
for (folderId in otherFolderIds) {
deleteFolder(folderId)
}
}
private fun getOtherFolders(folderType: String, excludeFolderId: Long): List<Long> {
return db.query(
"folders",
arrayOf("id"),
"local_only = 1 AND type = ? AND id != ?",
arrayOf(folderType, excludeFolderId.toString()),
null,
null,
null
).use { cursor ->
cursor.map { cursor.getLong(0) }
}
}
private fun moveMessages(folderType: String, destinationFolderId: Long) {
val sourceFolderIds = getOtherFolders(folderType, destinationFolderId)
for (sourceFolderId in sourceFolderIds) {
moveMessages(sourceFolderId, destinationFolderId)
deleteFolder(sourceFolderId)
}
}
private fun moveMessages(sourceFolderId: Long, destinationFolderId: Long) {
Timber.v(" Moving messages from folder [$sourceFolderId] to folder [$destinationFolderId]…")
val values = ContentValues().apply {
put("folder_id", destinationFolderId)
}
val rows = db.update("messages", values, "folder_id = ?", arrayOf(sourceFolderId.toString()))
Timber.v(" $rows messages moved.")
}
private fun deleteFolder(folderId: Long) {
Timber.v(" Deleting folder [$folderId]")
db.delete("folders", "id = ?", arrayOf(folderId.toString()))
}
private fun Account.isPop3() = storeUri.startsWith("pop3")
companion object {
private const val OUTBOX_FOLDER_TYPE = "outbox"
private const val DRAFTS_FOLDER_TYPE = "drafts"
private const val SENT_FOLDER_TYPE = "sent"
private const val TRASH_FOLDER_TYPE = "trash"
}
}

View file

@ -21,5 +21,6 @@ object Migrations {
if (oldVersion < 73) MigrationTo73(db).rewritePendingCommandsToUseFolderIds()
if (oldVersion < 74) MigrationTo74(db, migrationsHelper.account).removeDeletedMessages()
if (oldVersion < 75) MigrationTo75(db, migrationsHelper).updateAccountWithSpecialFolderIds()
if (oldVersion < 76) MigrationTo76(db, migrationsHelper).cleanUpSpecialLocalFolders()
}
}

View file

@ -22,7 +22,8 @@ class FolderInfoHolder(
id = folderId,
serverId = localFolder.serverId,
name = localFolder.name,
type = getFolderType(account, folderId)
type = getFolderType(account, folderId),
isLocalOnly = localFolder.isLocalOnly
)
return folderNameFormatter.displayName(folder)
}

View file

@ -8,6 +8,7 @@ import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.activity.K9Activity
import com.fsck.k9.helper.EmailHelper.getDomainFromEmailAddress
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
import com.fsck.k9.preferences.Protocols
import com.fsck.k9.setup.ServerNameSuggester
import com.fsck.k9.ui.R
@ -22,6 +23,7 @@ import org.koin.android.ext.android.inject
class AccountSetupAccountType : K9Activity() {
private val preferences: Preferences by inject()
private val serverNameSuggester: ServerNameSuggester by inject()
private val localFoldersCreator: SpecialLocalFoldersCreator by inject()
private lateinit var account: Account
private var makeDefault = false
@ -52,6 +54,7 @@ class AccountSetupAccountType : K9Activity() {
private fun setupAccount(serverType: String, schemePrefix: String) {
setupStoreAndSmtpTransport(serverType, schemePrefix)
createSpecialLocalFolders()
returnAccountTypeSelectionResult()
}
@ -82,6 +85,10 @@ class AccountSetupAccountType : K9Activity() {
account.transportUri = transportUri.toString()
}
private fun createSpecialLocalFolders() {
localFoldersCreator.createSpecialLocalFolders(account)
}
private fun returnAccountTypeSelectionResult() {
AccountSetupIncoming.actionIncomingSettings(this, account, makeDefault)
finish()

View file

@ -33,6 +33,7 @@ import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator;
import com.fsck.k9.preferences.Protocols;
import com.fsck.k9.ui.R;
import com.fsck.k9.ui.ConnectionSettings;
@ -56,6 +57,7 @@ public class AccountSetupBasics extends K9Activity
private final BackendManager backendManager = DI.get(BackendManager.class);
private final ProvidersXmlDiscovery providersXmlDiscovery = DI.get(ProvidersXmlDiscovery.class);
private final AccountCreator accountCreator = DI.get(AccountCreator.class);
private final SpecialLocalFoldersCreator localFoldersCreator = DI.get(SpecialLocalFoldersCreator.class);
private EditText mEmailView;
private EditText mPasswordView;
@ -262,6 +264,8 @@ public class AccountSetupBasics extends K9Activity
mAccount.setDeletePolicy(accountCreator.getDefaultDeletePolicy(incomingServerSettings.type));
localFoldersCreator.createSpecialLocalFolders(mAccount);
// Check incoming here. Then check outgoing in onActivityResult()
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING);
}

View file

@ -27,7 +27,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.fsck.k9.Account;
import com.fsck.k9.Account.SpecialFolderSelection;
import com.fsck.k9.DI;
import com.fsck.k9.LocalKeyStoreManager;
import com.fsck.k9.Preferences;
@ -37,12 +36,9 @@ import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.FolderType;
import com.fsck.k9.mail.MailServerDirection;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStoreProvider;
import com.fsck.k9.ui.R;
import timber.log.Timber;
@ -438,8 +434,6 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
return null;
}
createSpecialLocalFolders(direction);
setResult(RESULT_OK);
finish();
@ -520,31 +514,6 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
return account.getStoreUri().startsWith("webdav");
}
private void createSpecialLocalFolders(CheckDirection direction) throws MessagingException {
if (direction != CheckDirection.INCOMING) {
return;
}
LocalStore localStore = DI.get(LocalStoreProvider.class).getInstance(account);
long outboxFolderId = localStore.createLocalFolder(Account.OUTBOX_NAME, FolderType.OUTBOX);
account.setOutboxFolderId(outboxFolderId);
if (!account.getStoreUri().startsWith("pop3")) {
return;
}
long draftsFolderId = localStore.createLocalFolder(
getString(R.string.special_mailbox_name_drafts), FolderType.DRAFTS);
long sentFolderId = localStore.createLocalFolder(
getString(R.string.special_mailbox_name_sent), FolderType.SENT);
long trashFolderId = localStore.createLocalFolder(
getString(R.string.special_mailbox_name_trash), FolderType.TRASH);
account.setDraftsFolderId(draftsFolderId, SpecialFolderSelection.MANUAL);
account.setSentFolderId(sentFolderId, SpecialFolderSelection.MANUAL);
account.setTrashFolderId(trashFolderId, SpecialFolderSelection.MANUAL);
}
@Override
protected void onProgressUpdate(Integer... values) {
setMessage(values[0]);

View file

@ -6,9 +6,24 @@ import com.fsck.k9.mailstore.FolderType
import com.fsck.k9.ui.R
class FolderNameFormatter(private val resources: Resources) {
fun displayName(folder: Folder): String = when (folder.type) {
FolderType.INBOX -> resources.getString(R.string.special_mailbox_name_inbox)
fun displayName(folder: Folder): String {
return if (folder.isLocalOnly) {
localFolderDisplayName(folder)
} else {
remoteFolderDisplayName(folder)
}
}
private fun localFolderDisplayName(folder: Folder) = when (folder.type) {
FolderType.OUTBOX -> resources.getString(R.string.special_mailbox_name_outbox)
FolderType.DRAFTS -> resources.getString(R.string.special_mailbox_name_drafts)
FolderType.SENT -> resources.getString(R.string.special_mailbox_name_sent)
FolderType.TRASH -> resources.getString(R.string.special_mailbox_name_trash)
else -> folder.name
}
private fun remoteFolderDisplayName(folder: Folder) = when (folder.type) {
FolderType.INBOX -> resources.getString(R.string.special_mailbox_name_inbox)
else -> folder.name
}
}

View file

@ -82,7 +82,8 @@ class FolderSettingsViewModel(
id = folder.id,
serverId = folder.serverId,
name = folder.name,
type = folderType
type = folderType,
isLocalOnly = folder.isLocalOnly
)
}