Add JMAP folder sync

This commit is contained in:
cketti 2019-12-17 02:35:43 +01:00
parent 148af8aae8
commit 0b21a7521d
14 changed files with 985 additions and 1 deletions

View file

@ -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 {

View file

@ -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)
}

View file

@ -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) {

View file

@ -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()
}

View file

@ -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"))
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -0,0 +1,26 @@
{
"methodResponses": [
[
"error",
{
"type": "cannotCalculateChanges"
},
"0"
],
[
"error",
{
"type": "resultReference"
},
"1"
],
[
"error",
{
"type": "resultReference"
},
"2"
]
],
"sessionState": "0"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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);
}