Add JMAP folder sync
This commit is contained in:
parent
148af8aae8
commit
0b21a7521d
14 changed files with 985 additions and 1 deletions
|
@ -17,6 +17,7 @@ dependencies {
|
|||
testImplementation project(":mail:testing")
|
||||
testImplementation "junit:junit:${versions.junit}"
|
||||
testImplementation "org.mockito:mockito-core:${versions.mockito}"
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendStorage
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import java.lang.Exception
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
import rs.ltt.jmap.client.api.ErrorResponseException
|
||||
import rs.ltt.jmap.client.api.InvalidSessionResourceException
|
||||
import rs.ltt.jmap.client.api.MethodErrorResponseException
|
||||
import rs.ltt.jmap.client.api.UnauthorizedException
|
||||
import rs.ltt.jmap.common.Request.Invocation.ResultReference
|
||||
import rs.ltt.jmap.common.entity.Mailbox
|
||||
import rs.ltt.jmap.common.entity.Role
|
||||
import rs.ltt.jmap.common.method.call.mailbox.ChangesMailboxMethodCall
|
||||
import rs.ltt.jmap.common.method.call.mailbox.GetMailboxMethodCall
|
||||
import rs.ltt.jmap.common.method.response.mailbox.ChangesMailboxMethodResponse
|
||||
import rs.ltt.jmap.common.method.response.mailbox.GetMailboxMethodResponse
|
||||
|
||||
internal class CommandRefreshFolderList(
|
||||
private val backendStorage: BackendStorage,
|
||||
private val jmapClient: JmapClient,
|
||||
private val accountId: String
|
||||
) {
|
||||
fun refreshFolderList() {
|
||||
try {
|
||||
val state = backendStorage.getExtraString(STATE)
|
||||
if (state == null) {
|
||||
fetchMailboxes()
|
||||
} else {
|
||||
fetchMailboxUpdates(state)
|
||||
}
|
||||
} catch (e: UnauthorizedException) {
|
||||
throw AuthenticationFailedException("Authentication failed", e)
|
||||
} catch (e: InvalidSessionResourceException) {
|
||||
throw MessagingException(e.message, true, e)
|
||||
} catch (e: ErrorResponseException) {
|
||||
throw MessagingException(e.message, true, e)
|
||||
} catch (e: MethodErrorResponseException) {
|
||||
throw MessagingException(e.message, e.isPermanentError, e)
|
||||
} catch (e: Exception) {
|
||||
throw MessagingException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMailboxes() {
|
||||
val call = jmapClient.call(GetMailboxMethodCall(accountId))
|
||||
val response = call.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
val foldersOnServer = response.list
|
||||
|
||||
val oldFolderServerIds = backendStorage.getFolderServerIds()
|
||||
val (foldersToUpdate, foldersToCreate) = foldersOnServer.partition { it.id in oldFolderServerIds }
|
||||
|
||||
for (folder in foldersToUpdate) {
|
||||
backendStorage.changeFolder(folder.id, folder.name, folder.type)
|
||||
}
|
||||
|
||||
val newFolders = foldersToCreate.map { folder ->
|
||||
FolderInfo(folder.id, folder.name, folder.type)
|
||||
}
|
||||
backendStorage.createFolders(newFolders)
|
||||
|
||||
val newFolderServerIds = foldersOnServer.map { it.id }
|
||||
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
|
||||
backendStorage.deleteFolders(removedFolderServerIds)
|
||||
|
||||
backendStorage.setExtraString(STATE, response.state)
|
||||
}
|
||||
|
||||
private fun fetchMailboxUpdates(state: String) {
|
||||
try {
|
||||
fetchAllMailboxChanges(state)
|
||||
} catch (e: MethodErrorResponseException) {
|
||||
if (e.methodErrorResponse.type == ERROR_CANNOT_CALCULATE_CHANGES) {
|
||||
fetchMailboxes()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAllMailboxChanges(state: String) {
|
||||
var currentState = state
|
||||
do {
|
||||
val (newState, hasMoreChanges) = fetchMailboxChanges(currentState)
|
||||
currentState = newState
|
||||
} while (hasMoreChanges)
|
||||
}
|
||||
|
||||
private fun fetchMailboxChanges(state: String): UpdateState {
|
||||
val multiCall = jmapClient.newMultiCall()
|
||||
val mailboxChangesCall = multiCall.call(ChangesMailboxMethodCall(accountId, state))
|
||||
val createdMailboxesCall = multiCall.call(
|
||||
GetMailboxMethodCall(
|
||||
accountId,
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.CREATED)
|
||||
)
|
||||
)
|
||||
val changedMailboxesCall = multiCall.call(
|
||||
GetMailboxMethodCall(
|
||||
accountId,
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.UPDATED),
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.UPDATED_PROPERTIES)
|
||||
)
|
||||
)
|
||||
multiCall.execute()
|
||||
|
||||
val mailboxChangesResponse = mailboxChangesCall.getMainResponseBlocking<ChangesMailboxMethodResponse>()
|
||||
val createdMailboxResponse = createdMailboxesCall.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
val changedMailboxResponse = changedMailboxesCall.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
|
||||
val foldersToCreate = createdMailboxResponse.list.map { folder ->
|
||||
FolderInfo(folder.id, folder.name, folder.type)
|
||||
}
|
||||
backendStorage.createFolders(foldersToCreate)
|
||||
|
||||
for (folder in changedMailboxResponse.list) {
|
||||
backendStorage.changeFolder(folder.id, folder.name, folder.type)
|
||||
}
|
||||
|
||||
val destroyed = mailboxChangesResponse.destroyed
|
||||
destroyed?.let {
|
||||
backendStorage.deleteFolders(it.toList())
|
||||
}
|
||||
|
||||
backendStorage.setExtraString(STATE, mailboxChangesResponse.newState)
|
||||
|
||||
return UpdateState(
|
||||
state = mailboxChangesResponse.newState,
|
||||
hasMoreChanges = mailboxChangesResponse.isHasMoreChanges
|
||||
)
|
||||
}
|
||||
|
||||
private val Mailbox.type: FolderType
|
||||
get() = when (role) {
|
||||
Role.INBOX -> FolderType.INBOX
|
||||
Role.ARCHIVE -> FolderType.ARCHIVE
|
||||
Role.DRAFTS -> FolderType.DRAFTS
|
||||
Role.SENT -> FolderType.SENT
|
||||
Role.TRASH -> FolderType.TRASH
|
||||
Role.JUNK -> FolderType.SPAM
|
||||
else -> FolderType.REGULAR
|
||||
}
|
||||
|
||||
private val MethodErrorResponseException.isPermanentError: Boolean
|
||||
get() = methodErrorResponse.type != ERROR_SERVER_UNAVAILABLE
|
||||
|
||||
companion object {
|
||||
private const val STATE = "jmapState"
|
||||
private const val ERROR_SERVER_UNAVAILABLE = "serverUnavailable"
|
||||
private const val ERROR_CANNOT_CALCULATE_CHANGES = "cannotCalculateChanges"
|
||||
}
|
||||
|
||||
private data class UpdateState(val state: String, val hasMoreChanges: Boolean)
|
||||
}
|
|
@ -21,6 +21,7 @@ class JmapBackend(
|
|||
) : Backend {
|
||||
private val jmapClient = config.toJmapClient()
|
||||
private val accountId = config.accountId
|
||||
private val commandRefreshFolderList = CommandRefreshFolderList(backendStorage, jmapClient, accountId)
|
||||
override val supportsSeenFlag = true
|
||||
override val supportsExpunge = false
|
||||
override val supportsMove = true
|
||||
|
@ -32,7 +33,7 @@ class JmapBackend(
|
|||
override val isDeleteMoveToTrash = false
|
||||
|
||||
override fun refreshFolderList() {
|
||||
// TODO: implement
|
||||
commandRefreshFolderList.refreshFolderList()
|
||||
}
|
||||
|
||||
override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import rs.ltt.jmap.client.JmapRequest
|
||||
import rs.ltt.jmap.client.MethodResponses
|
||||
import rs.ltt.jmap.common.method.MethodResponse
|
||||
|
||||
internal inline fun <reified T : MethodResponse> ListenableFuture<MethodResponses>.getMainResponseBlocking(): T {
|
||||
return try {
|
||||
get().getMain(T::class.java)
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified T : MethodResponse> JmapRequest.Call.getMainResponseBlocking(): T {
|
||||
return methodResponses.getMainResponseBlocking()
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import junit.framework.AssertionFailedError
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
|
||||
class CommandRefreshFolderListTest {
|
||||
private val backendStorage = InMemoryBackendStorage()
|
||||
|
||||
@Test
|
||||
fun sessionResourceWithAuthenticationError() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
MockResponse().setResponseCode(401)
|
||||
)
|
||||
|
||||
try {
|
||||
command.refreshFolderList()
|
||||
fail("Expected exception")
|
||||
} catch (e: AuthenticationFailedException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidSessionResource() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
MockResponse().setBody("invalid")
|
||||
)
|
||||
|
||||
try {
|
||||
command.refreshFolderList()
|
||||
fail("Expected exception")
|
||||
} catch (e: MessagingException) {
|
||||
assertTrue(e.isPermanentFailure)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxes() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
|
||||
)
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
|
||||
assertMailboxState("23")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes.json")
|
||||
)
|
||||
createFoldersInBackendStorage(state = "23")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
|
||||
assertMailboxState("42")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates_withHasMoreChanges() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_1.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_2.json")
|
||||
)
|
||||
createFoldersInBackendStorage(state = "23")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
|
||||
assertMailboxState("42")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates_withCannotCalculateChangesError() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_error_cannot_calculate_changes.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
|
||||
)
|
||||
setMailboxState("unknownToServer")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
|
||||
assertMailboxState("23")
|
||||
}
|
||||
|
||||
private fun createCommandRefreshFolderList(vararg mockResponses: MockResponse): CommandRefreshFolderList {
|
||||
val server = createMockWebServer(*mockResponses)
|
||||
return createCommandRefreshFolderList(server.url("/jmap/"))
|
||||
}
|
||||
|
||||
private fun createMockWebServer(vararg mockResponses: MockResponse): MockWebServer {
|
||||
return MockWebServer().apply {
|
||||
for (mockResponse in mockResponses) {
|
||||
enqueue(mockResponse)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCommandRefreshFolderList(
|
||||
baseUrl: HttpUrl,
|
||||
accountId: String = "test@example.com"
|
||||
): CommandRefreshFolderList {
|
||||
val jmapClient = JmapClient("test", "test", baseUrl)
|
||||
return CommandRefreshFolderList(backendStorage, jmapClient, accountId)
|
||||
}
|
||||
|
||||
private fun responseBodyFromResource(name: String): MockResponse {
|
||||
return MockResponse().setBody(loadResource(name))
|
||||
}
|
||||
|
||||
private fun loadResource(name: String): String {
|
||||
val resourceAsStream = javaClass.getResourceAsStream(name) ?: error("Couldn't load resource: $name")
|
||||
return resourceAsStream.use { it.source().buffer().readUtf8() }
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun createFoldersInBackendStorage(state: String) {
|
||||
createFolderInBackendStorage("id_inbox", "Inbox", FolderType.INBOX)
|
||||
createFolderInBackendStorage("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
createFolderInBackendStorage("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
createFolderInBackendStorage("id_sent", "Sent", FolderType.SENT)
|
||||
createFolderInBackendStorage("id_trash", "Trash", FolderType.TRASH)
|
||||
createFolderInBackendStorage("id_folder1", "folder1", FolderType.REGULAR)
|
||||
setMailboxState(state)
|
||||
}
|
||||
|
||||
private fun createFolderInBackendStorage(serverId: String, name: String, type: FolderType) {
|
||||
backendStorage.createFolders(listOf(FolderInfo(serverId, name, type)))
|
||||
}
|
||||
|
||||
private fun setMailboxState(state: String) {
|
||||
backendStorage.setExtraString("jmapState", state)
|
||||
}
|
||||
|
||||
private fun assertFolderList(vararg folderServerIds: String) {
|
||||
assertEquals(folderServerIds.toSet(), backendStorage.getFolderServerIds().toSet())
|
||||
}
|
||||
|
||||
private fun assertFolderPresent(serverId: String, name: String, type: FolderType) {
|
||||
val folder = backendStorage.folders[serverId]
|
||||
?: throw AssertionFailedError("Expected folder '$serverId' in BackendStorage")
|
||||
|
||||
assertEquals(name, folder.name)
|
||||
assertEquals(type, folder.type)
|
||||
}
|
||||
|
||||
private fun assertMailboxState(expected: String) {
|
||||
assertEquals(expected, backendStorage.getExtraString("jmapState"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendFolder
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.Message
|
||||
import java.util.Date
|
||||
|
||||
class InMemoryBackendFolder(override var name: String, var type: FolderType) : BackendFolder {
|
||||
val extraStrings: MutableMap<String, String> = mutableMapOf()
|
||||
val extraNumbers: MutableMap<String, Long> = mutableMapOf()
|
||||
|
||||
override var visibleLimit: Int = 25
|
||||
|
||||
override fun getAllMessagesAndEffectiveDates(): Map<String, Long?> {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun destroyMessages(messageServerIds: List<String>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getLastUid(): Long? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getMoreMessages(): BackendFolder.MoreMessages {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setMoreMessages(moreMessages: BackendFolder.MoreMessages) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getUnreadMessageCount(): Int {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setLastChecked(timestamp: Long) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setStatus(status: String?) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getPushState(): String? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setPushState(pushState: String?) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun isMessagePresent(messageServerId: String): Boolean {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getMessageFlags(messageServerId: String): Set<Flag> {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setMessageFlag(messageServerId: String, flag: Flag, value: Boolean) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun savePartialMessage(message: Message) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun saveCompleteMessage(message: Message) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getLatestOldMessageSeenTime(): Date {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setLatestOldMessageSeenTime(date: Date) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getOldestMessageDate(): Date? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getFolderExtraString(name: String): String? = extraStrings[name]
|
||||
|
||||
override fun setFolderExtraString(name: String, value: String) {
|
||||
extraStrings[name] = value
|
||||
}
|
||||
|
||||
override fun getFolderExtraNumber(name: String): Long? = extraNumbers[name]
|
||||
|
||||
override fun setFolderExtraNumber(name: String, value: Long) {
|
||||
extraNumbers[name] = value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendFolder
|
||||
import com.fsck.k9.backend.api.BackendStorage
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.FolderType
|
||||
|
||||
class InMemoryBackendStorage : BackendStorage {
|
||||
val folders: MutableMap<String, InMemoryBackendFolder> = mutableMapOf()
|
||||
val extraStrings: MutableMap<String, String> = mutableMapOf()
|
||||
val extraNumbers: MutableMap<String, Long> = mutableMapOf()
|
||||
|
||||
override fun getFolder(folderServerId: String): BackendFolder {
|
||||
return folders[folderServerId] ?: error("Folder $folderServerId not found")
|
||||
}
|
||||
|
||||
override fun getFolderServerIds(): List<String> {
|
||||
return folders.keys.toList()
|
||||
}
|
||||
|
||||
override fun createFolders(folders: List<FolderInfo>) {
|
||||
folders.forEach { folder ->
|
||||
if (this.folders.containsKey(folder.serverId)) error("Folder ${folder.serverId} already present")
|
||||
|
||||
this.folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteFolders(folderServerIds: List<String>) {
|
||||
for (folderServerId in folderServerIds) {
|
||||
folders.remove(folderServerId) ?: error("Folder $folderServerId not found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun changeFolder(folderServerId: String, name: String, type: FolderType) {
|
||||
val folder = folders[folderServerId] ?: error("Folder $folderServerId not found")
|
||||
folder.name = name
|
||||
folder.type = type
|
||||
}
|
||||
|
||||
override fun getExtraString(name: String): String? = extraStrings[name]
|
||||
|
||||
override fun setExtraString(name: String, value: String) {
|
||||
extraStrings[name] = value
|
||||
}
|
||||
|
||||
override fun getExtraNumber(name: String): Long? = extraNumbers[name]
|
||||
|
||||
override fun setExtraNumber(name: String, value: Long) {
|
||||
extraNumbers[name] = value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "23",
|
||||
"newState": "42",
|
||||
"hasMoreChanges": false,
|
||||
"created": [ "id_folder2" ],
|
||||
"updated": [ "id_trash" ],
|
||||
"destroyed": [ "id_folder1" ]
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_folder2",
|
||||
"name": "folder2",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Deleted messages",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "23",
|
||||
"newState": "27",
|
||||
"hasMoreChanges": true,
|
||||
"created": [ "id_folder2" ],
|
||||
"updated": [],
|
||||
"destroyed": []
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "27",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_folder2",
|
||||
"name": "folder2",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "27",
|
||||
"list": []
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "27",
|
||||
"newState": "42",
|
||||
"hasMoreChanges": false,
|
||||
"created": [],
|
||||
"updated": [ "id_trash" ],
|
||||
"destroyed": [ "id_folder1" ]
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": []
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Deleted messages",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "cannotCalculateChanges"
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "resultReference"
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "resultReference"
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "23",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_inbox",
|
||||
"name": "Inbox",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": false,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": false
|
||||
},
|
||||
"role": "inbox",
|
||||
"totalEmails": 238,
|
||||
"unreadEmails": 6,
|
||||
"totalThreads": 80,
|
||||
"unreadThreads": 4,
|
||||
"sortOrder": 1,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_archive",
|
||||
"name": "Archive",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "archive",
|
||||
"totalEmails": 295,
|
||||
"unreadEmails": 36,
|
||||
"totalThreads": 136,
|
||||
"unreadThreads": 17,
|
||||
"sortOrder": 3,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_drafts",
|
||||
"name": "Drafts",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "drafts",
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 4,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_sent",
|
||||
"name": "Sent",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "sent",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 5,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Trash",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_folder1",
|
||||
"name": "folder1",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"username": "test",
|
||||
"apiUrl": "/jmap/",
|
||||
"downloadUrl": "/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
|
||||
"uploadUrl": "/jmap/upload/{accountId}/",
|
||||
"eventSourceUrl": "/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
|
||||
"accounts": {
|
||||
"test@example.com": {
|
||||
"name": "test@example.com",
|
||||
"isPersonal": true,
|
||||
"isReadOnly": false,
|
||||
"accountCapabilities": {
|
||||
"urn:ietf:params:jmap:core": {},
|
||||
"urn:ietf:params:jmap:submission": {
|
||||
"maxDelayedSend": 44236800,
|
||||
"submissionExtensions": {
|
||||
"size": [
|
||||
"10240000"
|
||||
],
|
||||
"dsn": []
|
||||
}
|
||||
},
|
||||
"urn:ietf:params:jmap:mail": {
|
||||
"emailQuerySortOptions": [
|
||||
"receivedAt",
|
||||
"sentAt",
|
||||
"from",
|
||||
"id",
|
||||
"emailstate",
|
||||
"size",
|
||||
"subject",
|
||||
"to",
|
||||
"hasKeyword",
|
||||
"someInThreadHaveKeyword",
|
||||
"addedDates",
|
||||
"threadSize",
|
||||
"spamScore",
|
||||
"snoozedUntil"
|
||||
],
|
||||
"maxKeywordsPerEmail": 100,
|
||||
"maxSizeAttachmentsPerEmail": 10485760,
|
||||
"maxMailboxesPerEmail": 20,
|
||||
"mayCreateTopLevelMailbox": true,
|
||||
"maxSizeMailboxName": 500
|
||||
},
|
||||
"urn:ietf:params:jmap:vacationresponse": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"urn:ietf:params:jmap:core": {
|
||||
"maxSizeUpload": 1073741824,
|
||||
"maxConcurrentUpload": 5,
|
||||
"maxCallsInRequest": 50,
|
||||
"maxObjectsInGet": 4096,
|
||||
"maxObjectsInSet": 4096,
|
||||
"collationAlgorithms": []
|
||||
},
|
||||
"urn:ietf:params:jmap:submission": {},
|
||||
"urn:ietf:params:jmap:mail": {},
|
||||
"urn:ietf:params:jmap:vacationresponse": {}
|
||||
},
|
||||
"state": "0"
|
||||
}
|
|
@ -6,6 +6,10 @@ public class MessagingException extends Exception {
|
|||
|
||||
private boolean permanentFailure = false;
|
||||
|
||||
public MessagingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public MessagingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue