Add ImapBackendPusher that manages multiple ImapFolderPusher instances

This commit is contained in:
cketti 2021-06-13 00:51:23 +02:00
parent d62aa030c9
commit c8808ae447
7 changed files with 217 additions and 6 deletions

View file

@ -10,6 +10,7 @@ import com.fsck.k9.mail.NetworkType
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.ImapStore
import com.fsck.k9.mail.store.imap.ImapStoreConfig
import com.fsck.k9.mail.transport.smtp.SmtpTransport
@ -18,6 +19,7 @@ import com.fsck.k9.mailstore.K9BackendStorageFactory
class ImapBackendFactory(
private val context: Context,
private val powerManager: PowerManager,
private val idleRefreshManager: IdleRefreshManager,
private val backendStorageFactory: K9BackendStorageFactory,
private val trustedSocketFactory: TrustedSocketFactory
) : BackendFactory {
@ -26,7 +28,8 @@ class ImapBackendFactory(
val backendStorage = backendStorageFactory.createBackendStorage(account)
val imapStore = createImapStore(account)
val smtpTransport = createSmtpTransport(account)
return ImapBackend(accountName, backendStorage, imapStore, powerManager, smtpTransport)
return ImapBackend(accountName, backendStorage, imapStore, powerManager, idleRefreshManager, smtpTransport)
}
private fun createImapStore(account: Account): ImapStore {

View file

@ -1,7 +1,9 @@
package com.fsck.k9.backends
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import org.koin.dsl.module
val backendsModule = module {
@ -15,7 +17,16 @@ val backendsModule = module {
)
)
}
single { ImapBackendFactory(get(), get(), get(), get()) }
single {
ImapBackendFactory(
context = get(),
powerManager = get(),
idleRefreshManager = get(),
backendStorageFactory = get(),
trustedSocketFactory = get()
)
}
single<IdleRefreshManager> { BackendIdleRefreshManager() }
single { Pop3BackendFactory(get(), get()) }
single { WebDavBackendFactory(get(), get(), get()) }
single { JmapBackendFactory(get(), get()) }

View file

@ -10,6 +10,7 @@ import com.fsck.k9.mail.NetworkType
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.ImapStore
import com.fsck.k9.mail.store.imap.ImapStoreConfig
import com.fsck.k9.mail.transport.smtp.SmtpTransport
@ -18,6 +19,7 @@ import com.fsck.k9.mailstore.K9BackendStorageFactory
class ImapBackendFactory(
private val context: Context,
private val powerManager: PowerManager,
private val idleRefreshManager: IdleRefreshManager,
private val backendStorageFactory: K9BackendStorageFactory,
private val trustedSocketFactory: TrustedSocketFactory
) : BackendFactory {
@ -26,7 +28,8 @@ class ImapBackendFactory(
val backendStorage = backendStorageFactory.createBackendStorage(account)
val imapStore = createImapStore(account)
val smtpTransport = createSmtpTransport(account)
return ImapBackend(accountName, backendStorage, imapStore, powerManager, smtpTransport)
return ImapBackend(accountName, backendStorage, imapStore, powerManager, idleRefreshManager, smtpTransport)
}
private fun createImapStore(account: Account): ImapStore {

View file

@ -1,6 +1,8 @@
package com.fsck.k9.backends
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import org.koin.dsl.module
val backendsModule = module {
@ -13,7 +15,16 @@ val backendsModule = module {
)
)
}
single { ImapBackendFactory(get(), get(), get(), get()) }
single {
ImapBackendFactory(
context = get(),
powerManager = get(),
idleRefreshManager = get(),
backendStorageFactory = get(),
trustedSocketFactory = get()
)
}
single<IdleRefreshManager> { BackendIdleRefreshManager() }
single { Pop3BackendFactory(get(), get()) }
single { WebDavBackendFactory(get(), get(), get()) }
}

View file

@ -0,0 +1,14 @@
package com.fsck.k9.backend.imap
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.IdleRefreshTimer
class BackendIdleRefreshManager : IdleRefreshManager {
override fun startTimer(timeout: Long, callback: () -> Unit): IdleRefreshTimer {
TODO("implement")
}
override fun resetTimers() {
TODO("implement")
}
}

View file

@ -11,14 +11,16 @@ import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.Part
import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.ImapStore
import com.fsck.k9.mail.transport.smtp.SmtpTransport
class ImapBackend(
accountName: String,
private val accountName: String,
backendStorage: BackendStorage,
private val imapStore: ImapStore,
private val powerManager: PowerManager,
private val idleRefreshManager: IdleRefreshManager,
private val smtpTransport: SmtpTransport
) : Backend {
private val imapSync = ImapSync(accountName, backendStorage, imapStore)
@ -155,6 +157,6 @@ class ImapBackend(
}
override fun createPusher(callback: BackendPusherCallback): BackendPusher {
TODO("implement")
return ImapBackendPusher(imapStore, powerManager, idleRefreshManager, callback, accountName)
}
}

View file

@ -0,0 +1,167 @@
package com.fsck.k9.backend.imap
import com.fsck.k9.backend.api.BackendPusher
import com.fsck.k9.backend.api.BackendPusherCallback
import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.IdleRefreshTimer
import com.fsck.k9.mail.store.imap.ImapStore
import java.io.IOException
import timber.log.Timber
private const val IO_ERROR_TIMEOUT = 5 * 60 * 1000L
private const val UNEXPECTED_ERROR_TIMEOUT = 60 * 60 * 1000L
/**
* Manages [ImapFolderPusher] instances that listen for changes to individual folders.
*/
internal class ImapBackendPusher(
private val imapStore: ImapStore,
private val powerManager: PowerManager,
private val idleRefreshManager: IdleRefreshManager,
private val callback: BackendPusherCallback,
private val accountName: String
) : BackendPusher, ImapPusherCallback {
private val lock = Any()
private val pushFolders = mutableMapOf<String, ImapFolderPusher>()
private var currentFolderServerIds: Collection<String> = emptySet()
private val pushFolderSleeping = mutableMapOf<String, IdleRefreshTimer>()
override fun updateFolders(folderServerIds: Collection<String>) {
Timber.v("ImapBackendPusher.updateFolders(): %s", folderServerIds)
val stopFolderPushers: List<ImapFolderPusher>
val startFolderPushers: List<ImapFolderPusher>
synchronized(lock) {
currentFolderServerIds = folderServerIds
val oldRunningFolderServerIds = pushFolders.keys
val oldFolderServerIds = oldRunningFolderServerIds + pushFolderSleeping.keys
val removeFolderServerIds = oldFolderServerIds - folderServerIds
stopFolderPushers = removeFolderServerIds
.asSequence()
.onEach { folderServerId -> cancelRetryTimer(folderServerId) }
.map { folderServerId -> pushFolders.remove(folderServerId) }
.filterNotNull()
.toList()
val startFolderServerIds = folderServerIds - oldRunningFolderServerIds
startFolderPushers = startFolderServerIds
.asSequence()
.filterNot { folderServerId -> isWaitingForRetry(folderServerId) }
.onEach { folderServerId -> pushFolderSleeping.remove(folderServerId) }
.map { folderServerId ->
createImapFolderPusher(folderServerId).also { folderPusher ->
pushFolders[folderServerId] = folderPusher
}
}
.toList()
}
for (folderPusher in stopFolderPushers) {
folderPusher.stop()
}
for (folderPusher in startFolderPushers) {
folderPusher.start()
}
}
override fun stop() {
Timber.v("ImapBackendPusher.stop()")
synchronized(lock) {
for (pushFolder in pushFolders.values) {
pushFolder.stop()
}
pushFolders.clear()
for (retryTimer in pushFolderSleeping.values) {
retryTimer.cancel()
}
pushFolderSleeping.clear()
currentFolderServerIds = emptySet()
}
}
private fun createImapFolderPusher(folderServerId: String): ImapFolderPusher {
// TODO: use value from account settings
val idleRefreshTimeoutMs = 25 * 60 * 1000L
return ImapFolderPusher(
imapStore,
powerManager,
idleRefreshManager,
this,
accountName,
folderServerId,
idleRefreshTimeoutMs
)
}
override fun onPushEvent(folderServerId: String) {
callback.onPushEvent(folderServerId)
idleRefreshManager.resetTimers()
}
override fun onPushError(folderServerId: String, exception: Exception) {
synchronized(lock) {
pushFolders.remove(folderServerId)
when (exception) {
is AuthenticationFailedException -> {
Timber.v(exception, "Authentication failure when attempting to use IDLE")
// TODO: This could be happening because of too many connections to the host. Ideally we'd want to
// detect this case and use a lower timeout.
startRetryTimer(folderServerId, UNEXPECTED_ERROR_TIMEOUT)
}
is IOException -> {
Timber.v(exception, "I/O error while trying to use IDLE")
startRetryTimer(folderServerId, IO_ERROR_TIMEOUT)
}
is MessagingException -> {
Timber.v(exception, "MessagingException")
if (exception.isPermanentFailure) {
startRetryTimer(folderServerId, UNEXPECTED_ERROR_TIMEOUT)
} else {
startRetryTimer(folderServerId, IO_ERROR_TIMEOUT)
}
}
else -> {
Timber.v(exception, "Unexpected error")
startRetryTimer(folderServerId, UNEXPECTED_ERROR_TIMEOUT)
}
}
if (pushFolders.isEmpty()) {
callback.onPushError(exception)
}
}
}
private fun startRetryTimer(folderServerId: String, timeout: Long) {
Timber.v("ImapBackendPusher for folder %s sleeping for %d ms", folderServerId, timeout)
pushFolderSleeping[folderServerId] = idleRefreshManager.startTimer(timeout, ::refresh)
}
private fun cancelRetryTimer(folderServerId: String) {
Timber.v("Canceling ImapBackendPusher retry timer for folder %s", folderServerId)
pushFolderSleeping.remove(folderServerId)?.cancel()
}
private fun isWaitingForRetry(folderServerId: String): Boolean {
return pushFolderSleeping[folderServerId]?.isWaiting == true
}
private fun refresh() {
Timber.v("Refreshing ImapBackendPusher (at least one retry timer has expired)")
val currentFolderServerIds = synchronized(lock) { currentFolderServerIds }
updateFolders(currentFolderServerIds)
}
}