Skip to content

Commit 588bf90

Browse files
authored
Merge pull request #9866 from rafaeltonholo/uplift/beta/9852/outbox-state-entry-not-found-enforce-outbox
uplift(beta): enforce always one outbox folder per account and remove outboxFolderId from preferences
2 parents a9d75ec + a947f00 commit 588bf90

File tree

58 files changed

+1068
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1068
-132
lines changed

app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountCreator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ internal class AccountCreator(
5454
}
5555
}
5656

57-
private fun create(account: Account): String {
57+
private suspend fun create(account: Account): String {
5858
val newAccount = preferences.newAccount(account.uuid)
5959

6060
newAccount.email = account.emailAddress

core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccount.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,6 @@ open class LegacyAccount(
163163
@set:Synchronized
164164
var inboxFolderId: Long? = null
165165

166-
@get:Synchronized
167-
@set:Synchronized
168-
var outboxFolderId: Long? = null
169-
170166
@get:Synchronized
171167
@set:Synchronized
172168
var draftsFolderId: Long? = null

core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapper.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ data class LegacyAccountWrapper(
5252
val importedArchiveFolder: String? = null,
5353
val importedSpamFolder: String? = null,
5454
val inboxFolderId: Long? = null,
55-
val outboxFolderId: Long? = null,
5655
val draftsFolderId: Long? = null,
5756
val sentFolderId: Long? = null,
5857
val trashFolderId: Long? = null,

core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ class LegacyAccountWrapperTest {
127127
importedArchiveFolder = null,
128128
importedSpamFolder = null,
129129
inboxFolderId = null,
130-
outboxFolderId = null,
131130
draftsFolderId = null,
132131
sentFolderId = null,
133132
trashFolderId = null,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@file:OptIn(ExperimentalTime::class)
2+
3+
package net.thunderbird.core.common.cache
4+
5+
import kotlin.time.Clock
6+
import kotlin.time.Duration
7+
import kotlin.time.Duration.Companion.hours
8+
import kotlin.time.ExperimentalTime
9+
import kotlin.time.Instant
10+
11+
class TimeLimitedCache<TKey : Any, TValue : Any?>(
12+
private val clock: Clock = Clock.System,
13+
private val cache: MutableMap<TKey, Entry<TValue>> = mutableMapOf(),
14+
) : Cache<TKey, TimeLimitedCache.Entry<TValue>> {
15+
companion object {
16+
private val DEFAULT_EXPIRATION_TIME = 1.hours
17+
}
18+
19+
override fun get(key: TKey): Entry<TValue>? {
20+
recycle(key)
21+
return cache[key]
22+
}
23+
24+
fun getValue(key: TKey): TValue? = get(key)?.value
25+
26+
fun set(key: TKey, value: TValue, expiresIn: Duration = DEFAULT_EXPIRATION_TIME) {
27+
set(key, Entry(value, creationTime = clock.now(), expiresIn))
28+
}
29+
30+
override fun set(key: TKey, value: Entry<TValue>) {
31+
cache[key] = value
32+
}
33+
34+
override fun hasKey(key: TKey): Boolean {
35+
recycle(key)
36+
return key in cache
37+
}
38+
39+
override fun clear() {
40+
cache.clear()
41+
}
42+
43+
fun clearExpired() {
44+
cache.entries.removeAll { (_, entry) ->
45+
entry.expiresAt < clock.now()
46+
}
47+
}
48+
49+
private fun recycle(key: TKey) {
50+
val entry = cache[key] ?: return
51+
if (entry.expiresAt < clock.now()) {
52+
cache.remove(key)
53+
}
54+
}
55+
56+
data class Entry<TValue : Any?>(
57+
val value: TValue,
58+
val creationTime: Instant,
59+
val expiresIn: Duration,
60+
val expiresAt: Instant = creationTime + expiresIn,
61+
)
62+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package net.thunderbird.core.common.cache
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import assertk.assertions.isFalse
6+
import assertk.assertions.isNotNull
7+
import assertk.assertions.isNull
8+
import kotlin.test.Test
9+
import kotlin.time.Duration
10+
import kotlin.time.Duration.Companion.milliseconds
11+
import kotlin.time.ExperimentalTime
12+
import net.thunderbird.core.testing.TestClock
13+
14+
@OptIn(ExperimentalTime::class)
15+
class TimeLimitedCacheTest {
16+
17+
private val clock = TestClock()
18+
private val cache = TimeLimitedCache<String, String>(clock = clock)
19+
20+
@Test
21+
fun `getValue should return null when entry present and expired`() {
22+
// Arrange
23+
cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
24+
clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
25+
26+
// Act
27+
val result = cache.getValue(KEY)
28+
29+
// Assert
30+
assertThat(result).isNull()
31+
}
32+
33+
@Test
34+
fun `hasKey should answer false when cache has entry and validity expired`() {
35+
// Arrange
36+
cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
37+
clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
38+
39+
// Act
40+
val result = cache.hasKey(KEY)
41+
42+
// Assert
43+
assertThat(result).isFalse()
44+
}
45+
46+
@Test
47+
fun `should keep cache when time progresses within expiration`() {
48+
// Arrange
49+
cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
50+
clock.advanceTimeBy(EXPIRES_IN - 1.milliseconds)
51+
52+
// Act
53+
val result = cache.getValue(KEY)
54+
55+
// Assert
56+
assertThat(result).isEqualTo(VALUE)
57+
}
58+
59+
@Test
60+
fun `clearExpired should remove only expired entries`() {
61+
// Arrange
62+
cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
63+
cache.set(KEY_2, VALUE_2, expiresIn = EXPIRES_IN * 2)
64+
clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
65+
66+
// Act
67+
cache.clearExpired()
68+
69+
// Assert
70+
assertThat(cache.getValue(KEY)).isNull()
71+
assertThat(cache.getValue(KEY_2)).isEqualTo(VALUE_2)
72+
}
73+
74+
@Test
75+
fun `get should return Entry with correct metadata when not expired`() {
76+
// Arrange
77+
cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
78+
79+
// Act
80+
val entry = cache[KEY]
81+
82+
// Assert
83+
assertThat(entry).isNotNull()
84+
entry!!
85+
assertThat(entry.value).isEqualTo(VALUE)
86+
assertThat(entry.expiresIn).isEqualTo(EXPIRES_IN)
87+
assertThat(entry.expiresAt).isEqualTo(entry.creationTime + EXPIRES_IN)
88+
}
89+
90+
private companion object {
91+
const val KEY = "key"
92+
const val KEY_2 = "key2"
93+
const val VALUE = "value"
94+
const val VALUE_2 = "value2"
95+
val EXPIRES_IN: Duration = 500.milliseconds
96+
}
97+
}

feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/LegacyAccountStorageHandler.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ class LegacyAccountStorageHandler(
8282
importedSpamFolder = storage.getStringOrNull(keyGen.create("spamFolderName"))
8383

8484
inboxFolderId = storage.getStringOrNull(keyGen.create("inboxFolderId"))?.toLongOrNull()
85-
outboxFolderId = storage.getStringOrNull(keyGen.create("outboxFolderId"))?.toLongOrNull()
8685

8786
val draftsFolderId = storage.getStringOrNull(keyGen.create("draftsFolderId"))?.toLongOrNull()
8887
val draftsFolderSelection = getEnumStringPref<SpecialFolderSelection>(
@@ -349,7 +348,6 @@ class LegacyAccountStorageHandler(
349348
editor.putString(keyGen.create("archiveFolderName"), importedArchiveFolder)
350349
editor.putString(keyGen.create("spamFolderName"), importedSpamFolder)
351350
editor.putString(keyGen.create("inboxFolderId"), inboxFolderId?.toString())
352-
editor.putString(keyGen.create("outboxFolderId"), outboxFolderId?.toString())
353351
editor.putString(keyGen.create("draftsFolderId"), draftsFolderId?.toString())
354352
editor.putString(keyGen.create("sentFolderId"), sentFolderId?.toString())
355353
editor.putString(keyGen.create("trashFolderId"), trashFolderId?.toString())

feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapper.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ class DefaultLegacyAccountWrapperDataMapper : DataMapper<LegacyAccountWrapper, L
4343
importedArchiveFolder = dto.importedArchiveFolder,
4444
importedSpamFolder = dto.importedSpamFolder,
4545
inboxFolderId = dto.inboxFolderId,
46-
outboxFolderId = dto.outboxFolderId,
4746
draftsFolderId = dto.draftsFolderId,
4847
sentFolderId = dto.sentFolderId,
4948
trashFolderId = dto.trashFolderId,
@@ -150,7 +149,6 @@ class DefaultLegacyAccountWrapperDataMapper : DataMapper<LegacyAccountWrapper, L
150149
importedArchiveFolder = domain.importedArchiveFolder
151150
importedSpamFolder = domain.importedSpamFolder
152151
inboxFolderId = domain.inboxFolderId
153-
outboxFolderId = domain.outboxFolderId
154152
draftsFolderId = domain.draftsFolderId
155153
sentFolderId = domain.sentFolderId
156154
trashFolderId = domain.trashFolderId

feature/account/storage/legacy/src/test/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapperTest.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
7777
assertThat(result.importedArchiveFolder).isEqualTo("importedArchiveFolder")
7878
assertThat(result.importedSpamFolder).isEqualTo("importedSpamFolder")
7979
assertThat(result.inboxFolderId).isEqualTo(1)
80-
assertThat(result.outboxFolderId).isEqualTo(2)
8180
assertThat(result.draftsFolderId).isEqualTo(3)
8281
assertThat(result.sentFolderId).isEqualTo(4)
8382
assertThat(result.trashFolderId).isEqualTo(5)
@@ -224,7 +223,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
224223
importedArchiveFolder = "importedArchiveFolder"
225224
importedSpamFolder = "importedSpamFolder"
226225
inboxFolderId = 1
227-
outboxFolderId = 2
228226
draftsFolderId = 3
229227
sentFolderId = 4
230228
trashFolderId = 5
@@ -339,7 +337,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
339337
importedArchiveFolder = "importedArchiveFolder",
340338
importedSpamFolder = "importedSpamFolder",
341339
inboxFolderId = 1,
342-
outboxFolderId = 2,
343340
draftsFolderId = 3,
344341
sentFolderId = 4,
345342
trashFolderId = 5,

feature/mail/folder/api/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ android {
99
kotlin {
1010
sourceSets {
1111
commonMain.dependencies {
12+
implementation(projects.core.outcome)
13+
implementation(projects.feature.account.api)
1214
implementation(projects.feature.mail.account.api)
15+
implementation(libs.androidx.annotation)
1316
}
1417
}
1518
}

0 commit comments

Comments
 (0)