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 fe29c2febb305a7dd96b8bd22e0f5584b0599587 Author: LanKhuat <[email protected]> AuthorDate: Thu Oct 8 13:38:24 2020 +0700 JAMES-3410 Email/set destroy implementation --- .../rfc8621/contract/EmailSetMethodContract.scala | 130 +++++++++++++++++++++ .../rfc8621/memory/MemoryEmailSetMethodTest.java | 39 +++++++ .../james/jmap/json/EmailSetSerializer.scala | 42 +++++++ .../org/apache/james/jmap/mail/EmailSet.scala | 33 ++++++ .../apache/james/jmap/method/EmailSetMethod.scala | 85 ++++++++++++++ 5 files changed, 329 insertions(+) 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 new file mode 100644 index 0000000..17f5bc3 --- /dev/null +++ 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 @@ -0,0 +1,130 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ +package org.apache.james.jmap.rfc8621.contract + +import java.nio.charset.StandardCharsets +import java.time.format.DateTimeFormatter +import java.util.concurrent.TimeUnit + +import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT +import io.restassured.RestAssured.{`given`, requestSpecification} +import io.restassured.http.ContentType.JSON +import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson +import org.apache.http.HttpStatus.SC_OK +import org.apache.james.GuiceJamesServer +import org.apache.james.jmap.http.UserCredential +import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder} +import org.apache.james.mailbox.MessageManager.AppendCommand +import org.apache.james.mailbox.model.{MailboxPath, MessageId} +import org.apache.james.mime4j.dom.Message +import org.apache.james.modules.MailboxProbeImpl +import org.apache.james.utils.DataProbeImpl +import org.awaitility.Awaitility +import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS +import org.junit.jupiter.api.{BeforeEach, Test} + +trait EmailSetMethodContract { + private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS + private lazy val calmlyAwait = Awaitility.`with` + .pollInterval(slowPacedPollInterval) + .and.`with`.pollDelay(slowPacedPollInterval) + .await + private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, TimeUnit.SECONDS) + + private lazy val UTC_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX") + + @BeforeEach + def setUp(server: GuiceJamesServer): Unit = { + server.getProbe(classOf[DataProbeImpl]) + .fluent + .addDomain(DOMAIN.asString) + .addUser(BOB.asString, BOB_PASSWORD) + .addUser(ANDRE.asString, ANDRE_PASSWORD) + + requestSpecification = baseRequestSpecBuilder(server) + .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD))) + .build + } + + @Test + def emailSetShouldDestroyEmail(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox(MailboxPath.inbox(BOB)) + val messageId1: MessageId = mailboxProbe + .appendMessage(BOB.asString, MailboxPath.inbox(BOB), + AppendCommand.from( + buildTestMessage)) + .getMessageId + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [ + | ["Email/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "destroy": ["${messageId1.serialize}"] + | }, "c1"], + | ["Email/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${messageId1.serialize}"] + | }, "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).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Email/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "destroyed": ["${messageId1.serialize}"] + | }, "c1"], + | ["Email/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [], + | "notFound": ["${messageId1.serialize}"] + | }, "c2"] + | ] + |}""".stripMargin) + } + + private def buildTestMessage = { + Message.Builder + .of + .setSubject("test") + .setBody("testmail", StandardCharsets.UTF_8) + .build + } +} diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java new file mode 100644 index 0000000..ec7126a --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java @@ -0,0 +1,39 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.rfc8621.memory; + +import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE; + +import org.apache.james.GuiceJamesServer; +import org.apache.james.JamesServerBuilder; +import org.apache.james.JamesServerExtension; +import org.apache.james.jmap.rfc8621.contract.EmailSetMethodContract; +import org.apache.james.modules.TestJMAPServerModule; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class MemoryEmailSetMethodTest implements EmailSetMethodContract { + + @RegisterExtension + static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE) + .overrideWith(new TestJMAPServerModule())) + .build(); +} 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 new file mode 100644 index 0000000..d4ce747 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.json + +import javax.inject.Inject +import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse} +import org.apache.james.mailbox.model.MessageId +import play.api.libs.json.{Format, JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OFormat, Reads, Writes} + +class EmailSetSerializer @Inject() (messageIdFactory: MessageId.Factory) { + + private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize) + private implicit val messageIdReads: Reads[MessageId] = { + case JsString(serializedMessageId) => JsSuccess(messageIdFactory.fromString(serializedMessageId)) + case _ => JsError("Invalid messageId") + } + + private implicit val destroyIdsFormat: Format[DestroyIds] = Json.valueFormat[DestroyIds] + private implicit val emailRequestSetFormat: Format[EmailSetRequest] = Json.format[EmailSetRequest] + private implicit val emailResponseSetFormat: OFormat[EmailSetResponse] = Json.format[EmailSetResponse] + + def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](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 new file mode 100644 index 0000000..a47ecff --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala @@ -0,0 +1,33 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ +package org.apache.james.jmap.mail + +import org.apache.james.jmap.model.AccountId +import org.apache.james.jmap.model.State.State +import org.apache.james.mailbox.model.MessageId + +case class DestroyIds(value: Seq[MessageId]) +case class EmailSetRequest(accountId: AccountId, + destroy: Option[DestroyIds]) + +case class EmailSetResponse(accountId: AccountId, + newState: State, + destroyed: Option[DestroyIds]) + + 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 new file mode 100644 index 0000000..9d086e4 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala @@ -0,0 +1,85 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ +package org.apache.james.jmap.method + +import eu.timepit.refined.auto._ +import javax.inject.Inject +import org.apache.james.jmap.http.SessionSupplier +import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer} +import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse} +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.{Capabilities, Invocation, State} +import org.apache.james.mailbox.model.DeleteResult +import org.apache.james.mailbox.{MailboxSession, MessageIdManager} +import org.apache.james.metrics.api.MetricFactory +import org.reactivestreams.Publisher +import play.api.libs.json.{JsError, JsSuccess} +import reactor.core.scala.publisher.SMono +import reactor.core.scheduler.Schedulers + +import scala.jdk.CollectionConverters._ + +class EmailSetMethod @Inject()(serializer: EmailSetSerializer, + messageIdManager: MessageIdManager, + val metricFactory: MetricFactory, + val sessionSupplier: SessionSupplier) extends Method { + + override val methodName: MethodName = MethodName("Email/set") + override val requiredCapabilities: Capabilities = Capabilities(CORE_CAPABILITY, MAIL_CAPABILITY) + + override def process(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): Publisher[InvocationWithContext] = { + asEmailSetRequest(invocation.invocation.arguments) + .flatMap(request => { + for { + destroyed <- destroy(request, mailboxSession) + } yield { + InvocationWithContext( + invocation = Invocation( + methodName = invocation.invocation.methodName, + arguments = Arguments(serializer.serialize(EmailSetResponse( + accountId = request.accountId, + newState = State.INSTANCE, + destroyed = destroyed))), + methodCallId = invocation.invocation.methodCallId), + processingContext = invocation.processingContext + ) + } + }) + } + + private def asEmailSetRequest(arguments: Arguments): SMono[EmailSetRequest] = { + serializer.deserialize(arguments.value) match { + case JsSuccess(request, _) => SMono.just(request) + case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString)) + } + } + + private def destroy(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[Option[DestroyIds]] = + emailSetRequest.destroy + .map(destroyId => deleteMessages(destroyId, mailboxSession)) + .getOrElse(SMono.just(None)) + + private def deleteMessages(destroyIds: DestroyIds, mailboxSession: MailboxSession): SMono[Option[DestroyIds]] = { + SMono.fromCallable[DeleteResult](() => messageIdManager.delete(destroyIds.value.asJava, mailboxSession)) + .subscribeOn(Schedulers.elastic) + .map(deleteResult => Some(DestroyIds(deleteResult.getDestroyed.asScala.toSeq))) + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
