Merge pull request #4545 from k9mail/jmap_set_flag
JMAP: Add support for setting flags/keywords
This commit is contained in:
commit
94ce631f7d
4 changed files with 128 additions and 10 deletions
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue