From 2a8d094343c231b4d7937f2080c21b2f11b7ce45 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 21 Feb 2020 01:51:10 +0100 Subject: [PATCH] JMAP: Add support for uploading messages --- app/core/build.gradle | 2 +- backend/jmap/build.gradle | 5 +- .../com/fsck/k9/backend/jmap/CommandUpload.kt | 98 +++++++++++++++++++ .../com/fsck/k9/backend/jmap/JmapBackend.kt | 5 +- .../k9/backend/jmap/JmapUploadResponse.kt | 11 +++ build.gradle | 1 + 6 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 backend/jmap/src/main/java/com/fsck/k9/backend/jmap/CommandUpload.kt create mode 100644 backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapUploadResponse.kt diff --git a/app/core/build.gradle b/app/core/build.gradle index 95da77811..799ec264a 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation "androidx.fragment:fragment:${versions.androidxFragment}" implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}" implementation "org.jsoup:jsoup:1.11.2" - implementation "com.squareup.moshi:moshi:1.9.2" + implementation "com.squareup.moshi:moshi:${versions.moshi}" implementation "com.jakewharton.timber:timber:${versions.timber}" implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" diff --git a/backend/jmap/build.gradle b/backend/jmap/build.gradle index c74ebd552..c6c008b44 100644 --- a/backend/jmap/build.gradle +++ b/backend/jmap/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-kapt' if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -12,8 +13,10 @@ dependencies { api project(":backend:api") api "com.squareup.okhttp3:okhttp:${versions.okhttp}" - implementation "rs.ltt.jmap:jmap-client:0.3.0" + implementation "rs.ltt.jmap:jmap-client:0.3.1" implementation "com.jakewharton.timber:timber:${versions.timber}" + implementation "com.squareup.moshi:moshi:${versions.moshi}" + kapt "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}" testImplementation project(":mail:testing") testImplementation "org.mockito:mockito-core:${versions.mockito}" diff --git a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/CommandUpload.kt b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/CommandUpload.kt new file mode 100644 index 000000000..2b7676756 --- /dev/null +++ b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/CommandUpload.kt @@ -0,0 +1,98 @@ +package com.fsck.k9.backend.jmap + +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.MessagingException +import com.squareup.moshi.Moshi +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okio.BufferedSink +import rs.ltt.jmap.client.JmapClient +import rs.ltt.jmap.client.http.HttpAuthentication +import rs.ltt.jmap.common.entity.EmailImport +import rs.ltt.jmap.common.method.call.email.ImportEmailMethodCall +import rs.ltt.jmap.common.method.response.email.ImportEmailMethodResponse +import timber.log.Timber + +class CommandUpload( + private val jmapClient: JmapClient, + private val okHttpClient: OkHttpClient, + private val httpAuthentication: HttpAuthentication, + private val accountId: String +) { + private val moshi = Moshi.Builder().build() + + fun uploadMessage(folderServerId: String, message: Message): String? { + Timber.d("Uploading message to $folderServerId") + + val uploadResponse = uploadMessageAsBlob(message) + return importEmailBlob(uploadResponse, folderServerId) + } + + private fun uploadMessageAsBlob(message: Message): JmapUploadResponse { + val session = jmapClient.session.get() + val uploadUrl = session.getUploadUrl(accountId) + + val request = Request.Builder() + .url(uploadUrl) + .post(MessageRequestBody(message)) + .apply { + httpAuthentication.authenticate(this) + } + .build() + + return okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw MessagingException("Uploading message as blob failed") + } + + response.body!!.source().use { source -> + val adapter = moshi.adapter(JmapUploadResponse::class.java) + val uploadResponse = adapter.fromJson(source) + uploadResponse ?: throw MessagingException("Error reading upload response") + } + } + } + + private fun importEmailBlob(uploadResponse: JmapUploadResponse, folderServerId: String): String? { + val importEmailRequest = ImportEmailMethodCall.builder() + .accountId(accountId) + .email( + LOCAL_EMAIL_ID, + EmailImport.builder() + .blobId(uploadResponse.blobId) + .keywords(mapOf("\$seen" to true)) + .mailboxIds(mapOf(folderServerId to true)) + .build() + ) + .build() + + val importEmailCall = jmapClient.call(importEmailRequest) + val importEmailResponse = importEmailCall.getMainResponseBlocking() + + return importEmailResponse.serverEmailId + } + + private val ImportEmailMethodResponse.serverEmailId + get() = created?.get(LOCAL_EMAIL_ID)?.id + + companion object { + private const val LOCAL_EMAIL_ID = "t1" + } +} + +private class MessageRequestBody(private val message: Message) : RequestBody() { + override fun contentType(): MediaType? { + return "message/rfc822".toMediaType() + } + + override fun contentLength(): Long { + return message.calculateSize() + } + + override fun writeTo(sink: BufferedSink) { + message.writeTo(sink.outputStream()) + } +} diff --git a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt index 8b2b7060b..1f4a42f63 100644 --- a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt +++ b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapBackend.kt @@ -31,6 +31,7 @@ class JmapBackend( private val commandSetFlag = CommandSetFlag(jmapClient, accountId) private val commandDelete = CommandDelete(jmapClient, accountId) private val commandMove = CommandMove(jmapClient, accountId) + private val commandUpload = CommandUpload(jmapClient, okHttpClient, httpAuthentication, accountId) override val supportsSeenFlag = true override val supportsExpunge = false override val supportsMove = true @@ -113,11 +114,11 @@ class JmapBackend( } override fun findByMessageId(folderServerId: String, messageId: String): String? { - throw UnsupportedOperationException("not implemented") + return null } override fun uploadMessage(folderServerId: String, message: Message): String? { - throw UnsupportedOperationException("not implemented") + return commandUpload.uploadMessage(folderServerId, message) } override fun createPusher(receiver: PushReceiver): Pusher { diff --git a/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapUploadResponse.kt b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapUploadResponse.kt new file mode 100644 index 000000000..f93b43ec0 --- /dev/null +++ b/backend/jmap/src/main/java/com/fsck/k9/backend/jmap/JmapUploadResponse.kt @@ -0,0 +1,11 @@ +package com.fsck.k9.backend.jmap + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class JmapUploadResponse( + val accountId: String, + val blobId: String, + val type: String, + val size: Long +) diff --git a/build.gradle b/build.gradle index 873d4da1c..bd401db02 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ buildscript { 'materialComponents': '1.1.0', 'preferencesFix': '1.1.0', 'okio': '2.4.3', + 'moshi': '1.9.2', 'timber': '4.5.1', 'koin': '2.0.1', 'commonsIo': '2.6',