Files
LibreMail/app/src/main/kotlin/org/libremail/di/DatabaseModule.kt
T
JMR-devandClaude Opus 4.8 0f479b2431 fix(mailbox): de-duplicate folder names in the drawer
The drawer rendered every standard-role folder with a generic friendly
name (e.g. "Drafts") and discarded the server name, so a Gmail account
with both a provider built-in folder and a same-named user folder showed
two identical entries (Drafts, Archive, Spam).

De-duplicate labels provider-agnostically: when 2+ folders would render
the same name, the provider's built-in special folder (identified by RFC
6154 SPECIAL-USE flags, now persisted on the folder cache) gets the
provider name appended ("Archive - Gmail"), a nested user folder gets its
parent location ("Reports (Work)"), and a top-level user folder keeps its
plain name. Only triggers on a real collision, so stock accounts are
unchanged.

Adds a `specialUse` column to the folders table (Room v11 -> v12).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:01:11 -05:00

115 lines
4.6 KiB
Kotlin

// SPDX-License-Identifier: GPL-3.0-or-later
package org.libremail.di
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
import org.libremail.data.local.DatabaseEncryption
import org.libremail.data.local.LibreMailDatabase
import org.libremail.data.local.MIGRATION_10_11
import org.libremail.data.local.MIGRATION_11_12
import org.libremail.data.local.MIGRATION_1_2
import org.libremail.data.local.MIGRATION_2_3
import org.libremail.data.local.MIGRATION_3_4
import org.libremail.data.local.MIGRATION_4_5
import org.libremail.data.local.MIGRATION_5_6
import org.libremail.data.local.MIGRATION_6_7
import org.libremail.data.local.MIGRATION_7_8
import org.libremail.data.local.MIGRATION_8_9
import org.libremail.data.local.MIGRATION_9_10
import org.libremail.data.local.dao.AccountDao
import org.libremail.data.local.dao.AccountSettingsDao
import org.libremail.data.local.dao.AttachmentDao
import org.libremail.data.local.dao.CredentialDao
import org.libremail.data.local.dao.DraftDao
import org.libremail.data.local.dao.FolderDao
import org.libremail.data.local.dao.MessageDao
import org.libremail.data.local.dao.OutboxDao
import org.libremail.data.local.dao.SignatureDao
import org.libremail.data.security.DatabaseKeyStore
import org.libremail.data.settings.SettingsRepository
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context,
keyStore: DatabaseKeyStore,
settingsRepository: SettingsRepository,
): LibreMailDatabase {
val builder = Room.databaseBuilder(context, LibreMailDatabase::class.java, DB_NAME)
.addMigrations(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10,
MIGRATION_10_11,
MIGRATION_11_12,
)
// No destructive fallback: the migration chain is complete, and silently dropping the
// accounts/credentials/mail tables would lose stored secrets. A missing migration should
// fail loudly in testing instead.
// Opt-in at-rest encryption of the local cache (off by default). The conversion runs here —
// before the database is opened — so it never races an open connection; toggling the setting
// therefore takes effect on the next app start. The passphrase is sealed by the Keystore.
val dbFile = context.getDatabasePath(DB_NAME)
if (runBlocking { settingsRepository.settings.first().encryptCache }) {
val passphrase = runBlocking { keyStore.passphrase() }
DatabaseEncryption.ensureEncrypted(dbFile, passphrase)
builder.openHelperFactory(
SupportOpenHelperFactory(passphrase.toByteArray(Charsets.US_ASCII), null, false),
)
} else if (DatabaseEncryption.isEncrypted(dbFile)) {
// Encryption was turned back off — decrypt so the default (unkeyed) open succeeds.
DatabaseEncryption.ensurePlaintext(dbFile, runBlocking { keyStore.passphrase() })
}
return builder.build()
}
@Provides
fun provideMessageDao(database: LibreMailDatabase): MessageDao = database.messageDao()
@Provides
fun provideAccountDao(database: LibreMailDatabase): AccountDao = database.accountDao()
@Provides
fun provideAccountSettingsDao(database: LibreMailDatabase): AccountSettingsDao = database.accountSettingsDao()
@Provides
fun provideCredentialDao(database: LibreMailDatabase): CredentialDao = database.credentialDao()
@Provides
fun provideAttachmentDao(database: LibreMailDatabase): AttachmentDao = database.attachmentDao()
@Provides
fun provideOutboxDao(database: LibreMailDatabase): OutboxDao = database.outboxDao()
@Provides
fun provideDraftDao(database: LibreMailDatabase): DraftDao = database.draftDao()
@Provides
fun provideFolderDao(database: LibreMailDatabase): FolderDao = database.folderDao()
@Provides
fun provideSignatureDao(database: LibreMailDatabase): SignatureDao = database.signatureDao()
private const val DB_NAME = "libremail.db"
}