You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/10/27 02:07:54 UTC
[james-project] 02/04: JAMES-3436 Email/set create - mailboxIds &
subject properties
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 339a016f6608f38ce651aa4f0f958135c176d97f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Oct 23 15:39:45 2020 +0700
JAMES-3436 Email/set create - mailboxIds & subject properties
---
.../rfc8621/contract/EmailSetMethodContract.scala | 58 ++++++++++
.../james/jmap/json/EmailSetSerializer.scala | 18 +++-
.../org/apache/james/jmap/mail/EmailSet.scala | 22 +++-
.../apache/james/jmap/method/EmailSetMethod.scala | 118 +++++++++++++++------
4 files changed, 179 insertions(+), 37 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 2898b92..d8a3e14 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -175,6 +175,64 @@ trait EmailSetMethodContract {
}
@Test
+ def createShouldAddAnEmailInTargetMailbox(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa":{
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "Boredome comes from a boring mind!"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["mailboxIds", "subject"]
+ | },
+ | "c2"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[1][1].list[0].id")
+ .inPath(s"methodResponses[1][1].list")
+ .isEqualTo(
+ s"""[{
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "Boredome comes from a boring mind!"
+ |}]""".stripMargin)
+ }
+
+ @Test
def shouldNotResetKeywordWhenInvalidKeyword(server: GuiceJamesServer): Unit = {
val message: Message = Fixture.createTestMessage
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 2928c03..e36e3ca 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -22,8 +22,9 @@ package org.apache.james.jmap.json
import cats.implicits._
import eu.timepit.refined.refineV
import javax.inject.Inject
-import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
-import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds}
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId, UnparsedMessageIdConstraint}
+import org.apache.james.jmap.mail.{DestroyIds, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, Subject}
+import org.apache.james.jmap.model.Id.IdConstraint
import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY
import org.apache.james.jmap.model.{Keyword, Keywords, SetError}
import org.apache.james.mailbox.model.{MailboxId, MessageId}
@@ -192,6 +193,13 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
case _ => JsError("Expecting a JsObject as an update entry")
})
+ private implicit val createsMapReads: Reads[Map[EmailCreationId, JsObject]] =
+ readMapEntry[EmailCreationId, JsObject](s => refineV[IdConstraint](s),
+ {
+ case o: JsObject => JsSuccess(o)
+ case _ => JsError("Expecting a JsObject as an update entry")
+ })
+
private implicit val keywordReads: Reads[Keyword] = {
case jsString: JsString => Keyword.parse(jsString.value)
.fold(JsError(_),
@@ -218,10 +226,16 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
}
private implicit val destroyIdsWrites: Writes[DestroyIds] = Json.valueWrites[DestroyIds]
private implicit val emailRequestSetReads: Reads[EmailSetRequest] = Json.reads[EmailSetRequest]
+ private implicit val emailCreationResponseWrites: Writes[EmailCreationResponse] = Json.writes[EmailCreationResponse]
private implicit val emailResponseSetWrites: OWrites[EmailSetResponse] = Json.writes[EmailSetResponse]
+ private implicit val subjectReads: Reads[Subject] = Json.valueReads[Subject]
+ private implicit val emailCreationRequestReads: Reads[EmailCreationRequest] = Json.reads[EmailCreationRequest]
+
def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](input)
+ def deserializeCreationRequest(input: JsValue): JsResult[EmailCreationRequest] = Json.fromJson[EmailCreationRequest](input)
+
def deserializeEmailSetUpdate(input: JsValue): JsResult[EmailSetUpdate] = Json.fromJson[EmailSetUpdate](input)
def serialize(response: EmailSetResponse): JsObject = Json.toJsObject(response)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ddeac39..5985147 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -18,19 +18,24 @@
****************************************************************/
package org.apache.james.jmap.mail
+import java.nio.charset.StandardCharsets
+
import eu.timepit.refined
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
-import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.jmap.model.Id.Id
import org.apache.james.jmap.model.State.State
import org.apache.james.jmap.model.{AccountId, Keywords, SetError}
import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mime4j.dom.Message
import play.api.libs.json.JsObject
import scala.util.{Right, Try}
object EmailSet {
+ type EmailCreationId = Id
type UnparsedMessageIdConstraint = NonEmpty
type UnparsedMessageId = String Refined UnparsedMessageIdConstraint
@@ -43,14 +48,27 @@ object EmailSet {
Try(messageIdFactory.fromString(unparsed.value))
}
+case class EmailCreationRequest(mailboxIds: MailboxIds,
+ subject: Option[Subject]) {
+ def toMime4JMessage: Message = {
+ val builder = Message.Builder.of
+ subject.foreach(value => builder.setSubject(value.value))
+ builder.setBody("", StandardCharsets.UTF_8)
+ builder.build()
+ }
+}
+
case class DestroyIds(value: Seq[UnparsedMessageId])
case class EmailSetRequest(accountId: AccountId,
+ create: Option[Map[EmailCreationId, JsObject]],
update: Option[Map[UnparsedMessageId, JsObject]],
destroy: Option[DestroyIds]) extends WithAccountId
case class EmailSetResponse(accountId: AccountId,
newState: State,
+ created: Option[Map[EmailCreationId, EmailCreationResponse]],
+ notCreated: Option[Map[EmailCreationId, SetError]],
updated: Option[Map[MessageId, Unit]],
notUpdated: Option[Map[UnparsedMessageId, SetError]],
destroyed: Option[DestroyIds],
@@ -118,3 +136,5 @@ class EmailUpdateValidationException() extends IllegalArgumentException
case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
+case class EmailCreationResponse(id: MessageId)
+
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 1d1b519..58f523d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -28,16 +28,20 @@ import org.apache.james.jmap.http.SessionSupplier
import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, MailboxIds, ValidatedEmailSetUpdate}
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
+import org.apache.james.jmap.mail.{DestroyIds, EmailCreationRequest, EmailCreationResponse, EmailSet, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, ValidatedEmailSetUpdate}
import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CAPABILITY}
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
import org.apache.james.jmap.model.SetError.SetErrorDescription
-import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
-import org.apache.james.mailbox.MessageManager.FlagsUpdateMode
+import org.apache.james.jmap.model.{Capabilities, ClientId, Id, Invocation, ServerId, SetError, State}
+import org.apache.james.mailbox.MessageManager.{AppendCommand, FlagsUpdateMode}
import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId, MessageRange}
import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageIdManager, MessageManager}
+import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageIdManager}
import org.apache.james.metrics.api.MetricFactory
import play.api.libs.json.{JsError, JsObject, JsSuccess}
import reactor.core.scala.publisher.{SFlux, SMono}
@@ -56,28 +60,20 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
case class DestroyResults(results: Seq[DestroyResult]) {
- def destroyed: Option[DestroyIds] = {
- Option(results.flatMap({
- result => result match {
+ def destroyed: Option[DestroyIds] =
+ Option(results.flatMap{
case result: DestroySuccess => Some(result.messageId)
case _ => None
- }
- }).map(EmailSet.asUnparsed))
+ }.map(EmailSet.asUnparsed))
.filter(_.nonEmpty)
.map(DestroyIds)
- }
- def notDestroyed: Option[Map[UnparsedMessageId, SetError]] = {
- Option(results.flatMap({
- result => result match {
- case failure: DestroyFailure => Some(failure)
+ def notDestroyed: Option[Map[UnparsedMessageId, SetError]] =
+ Option(results.flatMap{
+ case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
case _ => None
- }
- })
- .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
- .toMap)
+ }.toMap)
.filter(_.nonEmpty)
- }
}
object DestroyResult {
@@ -101,6 +97,31 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
}
}
+ case class CreationResults(results: Seq[CreationResult]) {
+ def created: Option[Map[EmailCreationId, EmailCreationResponse]] =
+ Option(results.flatMap{
+ case result: CreationSuccess => Some((result.clientId, result.response))
+ case _ => None
+ }.toMap)
+ .filter(_.nonEmpty)
+
+ def notCreated: Option[Map[EmailCreationId, SetError]] = {
+ Option(results.flatMap{
+ case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError))
+ case _ => None
+ }
+ .toMap)
+ .filter(_.nonEmpty)
+ }
+ }
+ trait CreationResult
+ case class CreationSuccess(clientId: EmailCreationId, response: EmailCreationResponse) extends CreationResult
+ case class CreationFailure(clientId: EmailCreationId, e: Throwable) extends CreationResult {
+ def asMessageSetError: SetError = e match {
+ case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ }
+ }
+
trait UpdateResult
case class UpdateSuccess(messageId: MessageId) extends UpdateResult
case class UpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends UpdateResult {
@@ -112,26 +133,19 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
}
}
case class UpdateResults(results: Seq[UpdateResult]) {
- def updated: Option[Map[MessageId, Unit]] = {
- Option(results.flatMap({
- result => result match {
+ def updated: Option[Map[MessageId, Unit]] =
+ Option(results.flatMap{
case result: UpdateSuccess => Some(result.messageId, ())
case _ => None
- }
- }).toMap).filter(_.nonEmpty)
- }
+ }.toMap)
+ .filter(_.nonEmpty)
- def notUpdated: Option[Map[UnparsedMessageId, SetError]] = {
- Option(results.flatMap({
- result => result match {
- case failure: UpdateFailure => Some(failure)
+ def notUpdated: Option[Map[UnparsedMessageId, SetError]] =
+ Option(results.flatMap{
+ case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
case _ => None
- }
- })
- .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
- .toMap)
+ }.toMap)
.filter(_.nonEmpty)
- }
}
override val methodName: MethodName = MethodName("Email/set")
@@ -140,19 +154,28 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSetRequest): SMono[InvocationWithContext] = {
for {
destroyResults <- destroy(request, mailboxSession)
- updateResults <- update(request, mailboxSession).doOnError(e => e.printStackTrace())
+ updateResults <- update(request, mailboxSession)
+ created <- create(request, mailboxSession)
} yield InvocationWithContext(
invocation = Invocation(
methodName = invocation.invocation.methodName,
arguments = Arguments(serializer.serialize(EmailSetResponse(
accountId = request.accountId,
newState = State.INSTANCE,
+ created = created.created,
+ notCreated = created.notCreated,
updated = updateResults.updated,
notUpdated = updateResults.notUpdated,
destroyed = destroyResults.destroyed,
notDestroyed = destroyResults.notDestroyed))),
methodCallId = invocation.invocation.methodCallId),
- processingContext = invocation.processingContext)
+ processingContext = created.created.getOrElse(Map())
+ .foldLeft(invocation.processingContext)({
+ case (processingContext, (clientId, response)) =>
+ Id.validate(response.id.serialize)
+ .fold(_ => processingContext,
+ serverId => processingContext.recordCreatedId(ClientId(clientId), ServerId(serverId)))
+ }))
}
override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments)
@@ -188,6 +211,33 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
}
}
+ private def create(request: EmailSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
+ SFlux.fromIterable(request.create.getOrElse(Map()))
+ .concatMap {
+ case (clientId, json) => serializer.deserializeCreationRequest(json)
+ .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new IllegalArgumentException(e.toString))),
+ creationRequest => create(clientId, creationRequest, mailboxSession))
+ }.collectSeq()
+ .map(CreationResults)
+
+ private def create(clientId: EmailCreationId, request: EmailCreationRequest, mailboxSession: MailboxSession): SMono[CreationResult] = {
+ if (request.mailboxIds.value.size != 1) {
+ SMono.just(CreationFailure(clientId, new IllegalArgumentException("mailboxIds need to have size 1")))
+ } else {
+ SMono.fromCallable[CreationResult](() => {
+ val mailboxId: MailboxId = request.mailboxIds.value.headOption.get
+ val appendResult = mailboxManager.getMailbox(mailboxId, mailboxSession)
+ .appendMessage(AppendCommand.builder()
+ .recent()
+ .build(request.toMime4JMessage),
+ mailboxSession)
+ CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId))
+ })
+ .subscribeOn(Schedulers.elastic())
+ .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
+ }
+ }
+
private def update(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[UpdateResults] = {
emailSetRequest.update
.filter(_.nonEmpty)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org