Add FolderFetcher
This commit is contained in:
parent
8f06c24bd3
commit
1de41d9177
9 changed files with 392 additions and 2 deletions
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.mail.folders
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
|
||||
/**
|
||||
* Fetches the list of folders from a server.
|
||||
*
|
||||
* @throws FolderFetcherException in case of an error
|
||||
*/
|
||||
fun interface FolderFetcher {
|
||||
fun getFolders(
|
||||
serverSettings: ServerSettings,
|
||||
authStateStorage: AuthStateStorage?,
|
||||
): List<RemoteFolder>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.fsck.k9.mail.folders
|
||||
|
||||
/**
|
||||
* Thrown by [FolderFetcher] in case of an error.
|
||||
*/
|
||||
class FolderFetcherException(
|
||||
cause: Throwable,
|
||||
val messageFromServer: String? = null,
|
||||
) : RuntimeException(cause.message, cause)
|
|
@ -0,0 +1,4 @@
|
|||
package com.fsck.k9.mail.folders
|
||||
|
||||
@JvmInline
|
||||
value class FolderServerId(val serverId: String)
|
|
@ -0,0 +1,9 @@
|
|||
package com.fsck.k9.mail.folders
|
||||
|
||||
import com.fsck.k9.mail.FolderType
|
||||
|
||||
data class RemoteFolder(
|
||||
val serverId: FolderServerId,
|
||||
val displayName: String,
|
||||
val type: FolderType,
|
||||
)
|
|
@ -0,0 +1,77 @@
|
|||
package com.fsck.k9.mail.store.imap
|
||||
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.folders.FolderFetcher
|
||||
import com.fsck.k9.mail.folders.FolderFetcherException
|
||||
import com.fsck.k9.mail.folders.FolderServerId
|
||||
import com.fsck.k9.mail.folders.RemoteFolder
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
|
||||
/**
|
||||
* Fetches the list of folders from an IMAP server.
|
||||
*/
|
||||
class ImapFolderFetcher internal constructor(
|
||||
private val trustedSocketFactory: TrustedSocketFactory,
|
||||
private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?,
|
||||
private val clientIdAppName: String,
|
||||
private val clientIdAppVersion: String,
|
||||
private val imapStoreFactory: ImapStoreFactory,
|
||||
) : FolderFetcher {
|
||||
constructor(
|
||||
trustedSocketFactory: TrustedSocketFactory,
|
||||
oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?,
|
||||
clientIdAppName: String,
|
||||
clientIdAppVersion: String,
|
||||
) : this(
|
||||
trustedSocketFactory,
|
||||
oAuth2TokenProviderFactory,
|
||||
clientIdAppName,
|
||||
clientIdAppVersion,
|
||||
imapStoreFactory = ImapStore.Companion,
|
||||
)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun getFolders(serverSettings: ServerSettings, authStateStorage: AuthStateStorage?): List<RemoteFolder> {
|
||||
require(serverSettings.type == "imap")
|
||||
|
||||
val config = object : ImapStoreConfig {
|
||||
override val logLabel = "folder-fetcher"
|
||||
override fun isSubscribedFoldersOnly() = false
|
||||
override fun clientId() = ImapClientId(appName = clientIdAppName, appVersion = clientIdAppVersion)
|
||||
}
|
||||
val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage)
|
||||
val store = imapStoreFactory.create(serverSettings, config, trustedSocketFactory, oAuth2TokenProvider)
|
||||
|
||||
return try {
|
||||
store.getFolders()
|
||||
.asSequence()
|
||||
.filterNot { it.oldServerId == null }
|
||||
.map { folder ->
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId(folder.oldServerId!!),
|
||||
displayName = folder.name,
|
||||
type = folder.type,
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
} catch (e: AuthenticationFailedException) {
|
||||
throw FolderFetcherException(messageFromServer = e.messageFromServer, cause = e)
|
||||
} catch (e: NegativeImapResponseException) {
|
||||
throw FolderFetcherException(messageFromServer = e.responseText, cause = e)
|
||||
} catch (e: Exception) {
|
||||
throw FolderFetcherException(cause = e)
|
||||
} finally {
|
||||
store.closeAllConnections()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOAuth2TokenProviderOrNull(authStateStorage: AuthStateStorage?): OAuth2TokenProvider? {
|
||||
return authStateStorage?.let {
|
||||
oAuth2TokenProviderFactory?.create(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,8 @@ interface ImapStore {
|
|||
|
||||
fun closeAllConnections()
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
companion object : ImapStoreFactory {
|
||||
override fun create(
|
||||
serverSettings: ServerSettings,
|
||||
config: ImapStoreConfig,
|
||||
trustedSocketFactory: TrustedSocketFactory,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.fsck.k9.mail.store.imap
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
|
||||
internal fun interface ImapStoreFactory {
|
||||
fun create(
|
||||
serverSettings: ServerSettings,
|
||||
config: ImapStoreConfig,
|
||||
trustedSocketFactory: TrustedSocketFactory,
|
||||
oauthTokenProvider: OAuth2TokenProvider?,
|
||||
): ImapStore
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.fsck.k9.mail.store.imap
|
||||
|
||||
import kotlin.test.fail
|
||||
|
||||
class FakeImapStore : ImapStore {
|
||||
private var openConnectionCount = 0
|
||||
|
||||
var getFoldersAction: () -> List<FolderListItem> = { fail("getFoldersAction not set") }
|
||||
val hasOpenConnections: Boolean
|
||||
get() = openConnectionCount != 0
|
||||
|
||||
override fun checkSettings() {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getFolder(name: String): ImapFolder {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getFolders(): List<FolderListItem> {
|
||||
openConnectionCount++
|
||||
return getFoldersAction()
|
||||
}
|
||||
|
||||
override fun closeAllConnections() {
|
||||
openConnectionCount = 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package com.fsck.k9.mail.store.imap
|
||||
|
||||
import assertk.assertFailure
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.prop
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.folders.FolderFetcherException
|
||||
import com.fsck.k9.mail.folders.FolderServerId
|
||||
import com.fsck.k9.mail.folders.RemoteFolder
|
||||
import com.fsck.k9.mail.helpers.FakeTrustManager
|
||||
import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponseList
|
||||
import kotlin.test.Test
|
||||
|
||||
class ImapFolderFetcherTest {
|
||||
private val fakeTrustManager = FakeTrustManager()
|
||||
private val trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager)
|
||||
private val fakeImapStore = FakeImapStore()
|
||||
private val folderFetcher = ImapFolderFetcher(
|
||||
trustedSocketFactory = trustedSocketFactory,
|
||||
oAuth2TokenProviderFactory = null,
|
||||
clientIdAppName = "irrelevant",
|
||||
clientIdAppVersion = "irrelevant",
|
||||
imapStoreFactory = { _, _, _, _ ->
|
||||
fakeImapStore
|
||||
},
|
||||
)
|
||||
private val serverSettings = ServerSettings(
|
||||
type = "imap",
|
||||
host = "irrelevant",
|
||||
port = 9999,
|
||||
connectionSecurity = ConnectionSecurity.NONE,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "irrelevant",
|
||||
password = "irrelevant",
|
||||
clientCertificateAlias = null,
|
||||
extra = ImapStoreSettings.createExtra(
|
||||
autoDetectNamespace = true,
|
||||
pathPrefix = null,
|
||||
useCompression = false,
|
||||
sendClientId = false,
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `regular folder list`() {
|
||||
fakeImapStore.getFoldersAction = {
|
||||
listOf(
|
||||
FolderListItem(
|
||||
serverId = "INBOX",
|
||||
name = "INBOX",
|
||||
type = FolderType.INBOX,
|
||||
oldServerId = "INBOX",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/All Mail",
|
||||
name = "[Gmail]/All Mail",
|
||||
type = FolderType.ARCHIVE,
|
||||
oldServerId = "[Gmail]/All Mail",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Drafts",
|
||||
name = "[Gmail]/Drafts",
|
||||
type = FolderType.DRAFTS,
|
||||
oldServerId = "[Gmail]/Drafts",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Important",
|
||||
name = "[Gmail]/Important",
|
||||
type = FolderType.REGULAR,
|
||||
oldServerId = "[Gmail]/Important",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Sent Mail",
|
||||
name = "[Gmail]/Sent Mail",
|
||||
type = FolderType.SENT,
|
||||
oldServerId = "[Gmail]/Sent Mail",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Spam",
|
||||
name = "[Gmail]/Spam",
|
||||
type = FolderType.SPAM,
|
||||
oldServerId = "[Gmail]/Spam",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Starred",
|
||||
name = "[Gmail]/Starred",
|
||||
type = FolderType.REGULAR,
|
||||
oldServerId = "[Gmail]/Starred",
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "[Gmail]/Trash",
|
||||
name = "[Gmail]/Trash",
|
||||
type = FolderType.TRASH,
|
||||
oldServerId = "[Gmail]/Trash",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val folders = folderFetcher.getFolders(serverSettings, authStateStorage = null)
|
||||
|
||||
assertThat(folders).containsExactly(
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("INBOX"),
|
||||
displayName = "INBOX",
|
||||
type = FolderType.INBOX,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/All Mail"),
|
||||
displayName = "[Gmail]/All Mail",
|
||||
type = FolderType.ARCHIVE,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Drafts"),
|
||||
displayName = "[Gmail]/Drafts",
|
||||
type = FolderType.DRAFTS,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Important"),
|
||||
displayName = "[Gmail]/Important",
|
||||
type = FolderType.REGULAR,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Sent Mail"),
|
||||
displayName = "[Gmail]/Sent Mail",
|
||||
type = FolderType.SENT,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Spam"),
|
||||
displayName = "[Gmail]/Spam",
|
||||
type = FolderType.SPAM,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Starred"),
|
||||
displayName = "[Gmail]/Starred",
|
||||
type = FolderType.REGULAR,
|
||||
),
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("[Gmail]/Trash"),
|
||||
displayName = "[Gmail]/Trash",
|
||||
type = FolderType.TRASH,
|
||||
),
|
||||
)
|
||||
assertThat(fakeImapStore.hasOpenConnections).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `folder without oldServerId should be ignored`() {
|
||||
fakeImapStore.getFoldersAction = {
|
||||
listOf(
|
||||
FolderListItem(
|
||||
serverId = "ünicode",
|
||||
name = "ünicode",
|
||||
type = FolderType.REGULAR,
|
||||
oldServerId = null,
|
||||
),
|
||||
FolderListItem(
|
||||
serverId = "INBOX",
|
||||
name = "INBOX",
|
||||
type = FolderType.INBOX,
|
||||
oldServerId = "INBOX",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val folders = folderFetcher.getFolders(serverSettings, authStateStorage = null)
|
||||
|
||||
assertThat(folders).containsExactly(
|
||||
RemoteFolder(
|
||||
serverId = FolderServerId("INBOX"),
|
||||
displayName = "INBOX",
|
||||
type = FolderType.INBOX,
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(fakeImapStore.hasOpenConnections).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authentication error should throw FolderFetcherException with server message`() {
|
||||
fakeImapStore.getFoldersAction = {
|
||||
throw AuthenticationFailedException(message = "Authentication failed", messageFromServer = "Server error")
|
||||
}
|
||||
|
||||
assertFailure {
|
||||
folderFetcher.getFolders(serverSettings, authStateStorage = null)
|
||||
}.isInstanceOf<FolderFetcherException>()
|
||||
.prop(FolderFetcherException::messageFromServer).isEqualTo("Server error")
|
||||
|
||||
assertThat(fakeImapStore.hasOpenConnections).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NegativeImapResponseException should throw FolderFetcherException with reply text as messageFromServer`() {
|
||||
fakeImapStore.getFoldersAction = {
|
||||
throw NegativeImapResponseException(
|
||||
message = "irrelevant",
|
||||
responses = createImapResponseList("x NO [NOPERM] Access denied"),
|
||||
)
|
||||
}
|
||||
|
||||
assertFailure {
|
||||
folderFetcher.getFolders(serverSettings, authStateStorage = null)
|
||||
}.isInstanceOf<FolderFetcherException>()
|
||||
.prop(FolderFetcherException::messageFromServer).isEqualTo("Access denied")
|
||||
|
||||
assertThat(fakeImapStore.hasOpenConnections).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unexpected exception should throw FolderFetcherException`() {
|
||||
fakeImapStore.getFoldersAction = {
|
||||
error("unexpected")
|
||||
}
|
||||
|
||||
assertFailure {
|
||||
folderFetcher.getFolders(serverSettings, authStateStorage = null)
|
||||
}.isInstanceOf<FolderFetcherException>()
|
||||
.prop(FolderFetcherException::messageFromServer).isNull()
|
||||
|
||||
assertThat(fakeImapStore.hasOpenConnections).isFalse()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue