Merge pull request #4545 from k9mail/jmap_set_flag

JMAP: Add support for setting flags/keywords
This commit is contained in:
cketti 2020-02-18 16:46:40 +01:00 committed by GitHub
commit 94ce631f7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 10 deletions

View file

@ -0,0 +1,107 @@
package com.fsck.k9.backend.jmap
import com.fsck.k9.mail.Flag
import rs.ltt.jmap.client.JmapClient
import rs.ltt.jmap.common.entity.filter.EmailFilterCondition
import rs.ltt.jmap.common.method.call.email.QueryEmailMethodCall
import rs.ltt.jmap.common.method.call.email.SetEmailMethodCall
import rs.ltt.jmap.common.method.response.email.QueryEmailMethodResponse
import rs.ltt.jmap.common.method.response.email.SetEmailMethodResponse
import rs.ltt.jmap.common.util.Patches
import timber.log.Timber
class CommandSetFlag(
private val jmapClient: JmapClient,
private val accountId: String
) {
fun setFlag(messageServerIds: List<String>, flag: Flag, newState: Boolean) {
if (newState) {
Timber.v("Setting flag %s for messages %s", flag, messageServerIds)
} else {
Timber.v("Removing flag %s for messages %s", flag, messageServerIds)
}
val keyword = flag.toKeyword()
val keywordsPatch = if (newState) {
Patches.set("keywords/$keyword", true)
} else {
Patches.remove("keywords/$keyword")
}
val session = jmapClient.session.get()
val maxObjectsInSet = session.maxObjectsInSet
messageServerIds.chunked(maxObjectsInSet).forEach { emailIds ->
val updates = emailIds.map { emailId ->
emailId to keywordsPatch
}.toMap()
val setEmailCall = jmapClient.call(
SetEmailMethodCall.builder()
.accountId(accountId)
.update(updates)
.build()
)
setEmailCall.getMainResponseBlocking<SetEmailMethodResponse>()
}
}
fun markAllAsRead(folderServerId: String) {
Timber.d("Marking all messages in %s as read", folderServerId)
val keywordsPatch = Patches.set("keywords/\$seen", true)
val session = jmapClient.session.get()
val limit = minOf(MAX_CHUNK_SIZE, session.maxObjectsInSet).toLong()
do {
Timber.v("Trying to mark up to %d messages in %s as read", limit, folderServerId)
val queryEmailCall = jmapClient.call(
QueryEmailMethodCall.builder()
.accountId(accountId)
.filter(EmailFilterCondition.builder()
.inMailbox(folderServerId)
.notKeyword("\$seen")
.build()
)
.calculateTotal(true)
.limit(limit)
.build()
)
val queryEmailResponse = queryEmailCall.getMainResponseBlocking<QueryEmailMethodResponse>()
val numberOfReturnedEmails = queryEmailResponse.ids.size
val totalNumberOfEmails = queryEmailResponse.total ?: error("Server didn't return property 'total'")
if (numberOfReturnedEmails == 0) {
Timber.v("There were no messages in %s to mark as read", folderServerId)
} else {
val updates = queryEmailResponse.ids.map { emailId ->
emailId to keywordsPatch
}.toMap()
val setEmailCall = jmapClient.call(
SetEmailMethodCall.builder()
.accountId(accountId)
.update(updates)
.build()
)
setEmailCall.getMainResponseBlocking<SetEmailMethodResponse>()
Timber.v("Marked %d messages in %s as read", numberOfReturnedEmails, folderServerId)
}
} while (totalNumberOfEmails > numberOfReturnedEmails)
}
private fun Flag.toKeyword(): String = when (this) {
Flag.SEEN -> "\$seen"
Flag.FLAGGED -> "\$flagged"
Flag.DRAFT -> "\$draft"
Flag.ANSWERED -> "\$answered"
Flag.FORWARDED -> "\$forwarded"
else -> error("Unsupported flag: $name")
}
}

View file

@ -17,7 +17,6 @@ import rs.ltt.jmap.client.api.UnauthorizedException
import rs.ltt.jmap.client.http.HttpAuthentication
import rs.ltt.jmap.client.session.Session
import rs.ltt.jmap.common.entity.Email
import rs.ltt.jmap.common.entity.capability.CoreCapability
import rs.ltt.jmap.common.entity.filter.EmailFilterCondition
import rs.ltt.jmap.common.entity.query.EmailQuery
import rs.ltt.jmap.common.method.call.email.GetEmailMethodCall
@ -173,7 +172,7 @@ class CommandSync(
Timber.d("New messages on server: %s", newServerIds)
val session = jmapClient.session.get()
val maxObjectsInGet = session.maxObjectsInGet()
val maxObjectsInGet = session.maxObjectsInGet
val messageInfoList = fetchMessageInfo(session, maxObjectsInGet, newServerIds)
val total = messageInfoList.size
@ -251,7 +250,7 @@ class CommandSync(
Timber.v("Fetching flags for messages: %s", emailIds)
val session = jmapClient.session.get()
val maxObjectsInGet = session.maxObjectsInGet()
val maxObjectsInGet = session.maxObjectsInGet
emailIds
.asSequence()
@ -296,11 +295,6 @@ class CommandSync(
else -> null
}
private fun Session.maxObjectsInGet(): Int {
val coreCapability = getCapability(CoreCapability::class.java)
return minOf(Int.MAX_VALUE.toLong(), coreCapability.maxObjectsInGet).toInt()
}
private fun BackendFolder.saveQueryState(queryState: String?) {
setFolderExtraString(EXTRA_QUERY_STATE, queryState)
}

View file

@ -28,6 +28,7 @@ class JmapBackend(
private val accountId = config.accountId
private val commandRefreshFolderList = CommandRefreshFolderList(backendStorage, jmapClient, accountId)
private val commandSync = CommandSync(backendStorage, jmapClient, okHttpClient, accountId, httpAuthentication)
private val commandSetFlag = CommandSetFlag(jmapClient, accountId)
override val supportsSeenFlag = true
override val supportsExpunge = false
override val supportsMove = true
@ -51,11 +52,11 @@ class JmapBackend(
}
override fun setFlag(folderServerId: String, messageServerIds: List<String>, flag: Flag, newState: Boolean) {
throw UnsupportedOperationException("not implemented")
commandSetFlag.setFlag(messageServerIds, flag, newState)
}
override fun markAllAsRead(folderServerId: String) {
throw UnsupportedOperationException("not implemented")
commandSetFlag.markAllAsRead(folderServerId)
}
override fun expunge(folderServerId: String) {

View file

@ -4,8 +4,12 @@ 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.client.session.Session
import rs.ltt.jmap.common.entity.capability.CoreCapability
import rs.ltt.jmap.common.method.MethodResponse
internal const val MAX_CHUNK_SIZE = 5000
internal inline fun <reified T : MethodResponse> ListenableFuture<MethodResponses>.getMainResponseBlocking(): T {
return futureGetOrThrow().getMain(T::class.java)
}
@ -21,3 +25,15 @@ internal inline fun <T> ListenableFuture<T>.futureGetOrThrow(): T {
throw e.cause ?: e
}
}
internal val Session.maxObjectsInGet: Int
get() {
val coreCapability = getCapability(CoreCapability::class.java)
return coreCapability.maxObjectsInGet.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
}
internal val Session.maxObjectsInSet: Int
get() {
val coreCapability = getCapability(CoreCapability::class.java)
return coreCapability.maxObjectsInSet.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
}