chibenwa commented on a change in pull request #385:
URL: https://github.com/apache/james-project/pull/385#discussion_r616378581
##########
File path: mdn/src/main/java/org/apache/james/mdn/MDN.java
##########
@@ -205,6 +206,10 @@ public MimeMultipart asMultipart() throws
MessagingException {
multipart.setReportType(DISPOSITION_NOTIFICATION_REPORT_TYPE);
multipart.addBodyPart(computeHumanReadablePart());
multipart.addBodyPart(computeReportPart());
+ if (message.isPresent()) {
+ multipart.addBodyPart(computeOriginalMessagePart());
+ }
Review comment:
```suggestion
message.ifPresent(originalMessage ->
multipart.addBodyPart(computeOriginalMessagePart(originalMessage)))
```
##########
File path: mdn/src/main/java/org/apache/james/mdn/MDN.java
##########
@@ -233,6 +238,17 @@ public BodyPart computeReportPart() throws
MessagingException {
return mdnPart;
}
+ public BodyPart computeOriginalMessagePart() throws MessagingException {
+ Preconditions.checkState(message.isPresent());
+ MimeBodyPart originalMessagePart = new MimeBodyPart();
+ try {
+ originalMessagePart.setText(new
String(DefaultMessageWriter.asBytes(message.get()), StandardCharsets.UTF_8));
Review comment:
I would prefer:
```suggestion
originalMessagePart.setContent(new
String(DefaultMessageWriter.asBytes(message.get()), StandardCharsets.UTF_8),
"message/rfc822");
```
Do we have unit tests for this method?
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDN.scala
##########
@@ -0,0 +1,131 @@
+/****************************************************************
+ * 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.core.MailAddress
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mdn.MDNReportParser
+import org.apache.james.mdn.`type`.DispositionType
+import org.apache.james.mdn.action.mode.DispositionActionMode
+import org.apache.james.mdn.fields.{FinalRecipient, ReportingUserAgent,
Disposition => JavaDisposition}
+import org.apache.james.mdn.sending.mode.DispositionSendingMode
+
+import java.util.Locale
+import scala.util.{Failure, Success, Try}
+
+object MDN {
+ val DISPOSITION_NOTIFICATION_TO: String = "Disposition-Notification-To"
+}
+
+case class MDNDispositionInvalidException(description: String) extends
Exception
+
+case class ForEmailIdField(originalMessageId: MessageId) extends AnyVal
+
+case class SubjectField(value: String) extends AnyVal
+
+case class TextBodyField(value: String) extends AnyVal
+
+case class ReportUAField(value: String) extends AnyVal {
+ def asJava: Try[ReportingUserAgent] = new MDNReportParser("Reporting-UA: " +
value)
+ .reportingUaField
+ .run()
+
+ def valid: Either[MDNSendRequestInvalidException, ReportUAField] =
Review comment:
```suggestion
def validate: Either[MDNSendRequestInvalidException, ReportUAField] =
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MDNSendMethod.scala
##########
@@ -0,0 +1,316 @@
+/****************************************************************
+ * 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 org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier,
JMAP_CORE, JMAP_MAIL, JMAP_MDN}
+import org.apache.james.jmap.core.Invocation
+import org.apache.james.jmap.core.Invocation._
+import org.apache.james.jmap.json.{MDNSerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.MDN._
+import org.apache.james.jmap.mail.MDNSend.MDN_ALREADY_SENT_FLAG
+import org.apache.james.jmap.mail._
+import org.apache.james.jmap.method.EmailSubmissionSetMethod.LOGGER
+import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
+import org.apache.james.lifecycle.api.Startable
+import org.apache.james.mailbox.model.{FetchGroup, MessageResult}
+import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
+import org.apache.james.mdn.fields.{ExtensionField, FinalRecipient, Text}
+import org.apache.james.mdn.{MDN, MDNReport}
+import org.apache.james.metrics.api.MetricFactory
+import org.apache.james.mime4j.codec.DecodeMonitor
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.mime4j.field.AddressListFieldLenientImpl
+import org.apache.james.mime4j.message.DefaultMessageBuilder
+import org.apache.james.mime4j.stream.MimeConfig
+import org.apache.james.queue.api.MailQueueFactory.SPOOL
+import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
+import org.apache.james.server.core.MailImpl
+import play.api.libs.json.{JsError, JsObject, JsSuccess, Json}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import javax.annotation.PreDestroy
+import javax.inject.Inject
+import javax.mail.internet.MimeMessage
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+import scala.util.Try
+
+class MDNSendMethod @Inject()(serializer: MDNSerializer,
+ mailQueueFactory: MailQueueFactory[_ <:
MailQueue],
+ messageIdManager: MessageIdManager,
+ emailSetMethod: EmailSetMethod,
+ val identifyResolver: IdentifyResolver,
+ val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier) extends
MethodRequiringAccountId[MDNSendRequest] with Startable {
+ override val methodName: MethodName = MethodName("MDN/send")
+ override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_MDN,
JMAP_MAIL, JMAP_CORE)
+ var queue: MailQueue = _
+
+ def init: Unit =
+ queue = mailQueueFactory.createQueue(SPOOL)
+
+ @PreDestroy def dispose: Unit =
+ Try(queue.close())
+ .recover(e => LOGGER.debug("error closing queue", e))
+
+ override def doProcess(capabilities: Set[CapabilityIdentifier],
+ invocation: InvocationWithContext,
+ mailboxSession: MailboxSession,
+ request: MDNSendRequest):
SFlux[InvocationWithContext] = {
+ identifyResolver.resolveIdentityId(request.identityId, mailboxSession)
+ .flatMap(maybeIdentity => if (maybeIdentity.isEmpty) {
+ SMono.raiseError(IdentityIdNotFoundException("The IdentityId cannot be
found"))
+ } else {
+ create(maybeIdentity.get, request, mailboxSession,
invocation.processingContext)
+ })
Review comment:
```suggestion
.flatMap(maybeIdentity =>maybeIdentity.map(identity =>
create(identity, request, mailboxSession, invocation.processingContext))
.getOrElse(SMono.raiseError(IdentityIdNotFoundException("The
IdentityId cannot be found")))
```
Calling .get is forbidden ;-)
##########
File path:
server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
##########
@@ -103,6 +105,8 @@ protected void configure() {
methods.addBinding().to(ThreadGetMethod.class);
methods.addBinding().to(VacationResponseGetMethod.class);
methods.addBinding().to(VacationResponseSetMethod.class);
+ methods.addBinding().to(MDNParseMethod.class);
Review comment:
MDNParse method is already added line 103.
Also please respect alphabetic ordering.
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MDNParseMethod.scala
##########
@@ -67,7 +68,7 @@ class MDNParseMethod @Inject()(val blobResolvers:
BlobResolvers,
computeResponse(request, mailboxSession)
.map(res => Invocation(
methodName,
- Arguments(MDNParseSerializer.serialize(res).as[JsObject]),
+ Arguments(serializer.serializeMDNParseResponse(res).as[JsObject]),
Review comment:
res => response
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MDNSendMethod.scala
##########
@@ -0,0 +1,316 @@
+/****************************************************************
+ * 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 org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier,
JMAP_CORE, JMAP_MAIL, JMAP_MDN}
+import org.apache.james.jmap.core.Invocation
+import org.apache.james.jmap.core.Invocation._
+import org.apache.james.jmap.json.{MDNSerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.MDN._
+import org.apache.james.jmap.mail.MDNSend.MDN_ALREADY_SENT_FLAG
+import org.apache.james.jmap.mail._
+import org.apache.james.jmap.method.EmailSubmissionSetMethod.LOGGER
+import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
+import org.apache.james.lifecycle.api.Startable
+import org.apache.james.mailbox.model.{FetchGroup, MessageResult}
+import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
+import org.apache.james.mdn.fields.{ExtensionField, FinalRecipient, Text}
+import org.apache.james.mdn.{MDN, MDNReport}
+import org.apache.james.metrics.api.MetricFactory
+import org.apache.james.mime4j.codec.DecodeMonitor
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.mime4j.field.AddressListFieldLenientImpl
+import org.apache.james.mime4j.message.DefaultMessageBuilder
+import org.apache.james.mime4j.stream.MimeConfig
+import org.apache.james.queue.api.MailQueueFactory.SPOOL
+import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
+import org.apache.james.server.core.MailImpl
+import play.api.libs.json.{JsError, JsObject, JsSuccess, Json}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import javax.annotation.PreDestroy
+import javax.inject.Inject
+import javax.mail.internet.MimeMessage
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+import scala.util.Try
+
+class MDNSendMethod @Inject()(serializer: MDNSerializer,
+ mailQueueFactory: MailQueueFactory[_ <:
MailQueue],
+ messageIdManager: MessageIdManager,
+ emailSetMethod: EmailSetMethod,
+ val identifyResolver: IdentifyResolver,
+ val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier) extends
MethodRequiringAccountId[MDNSendRequest] with Startable {
+ override val methodName: MethodName = MethodName("MDN/send")
+ override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_MDN,
JMAP_MAIL, JMAP_CORE)
+ var queue: MailQueue = _
+
+ def init: Unit =
+ queue = mailQueueFactory.createQueue(SPOOL)
+
+ @PreDestroy def dispose: Unit =
+ Try(queue.close())
+ .recover(e => LOGGER.debug("error closing queue", e))
+
+ override def doProcess(capabilities: Set[CapabilityIdentifier],
+ invocation: InvocationWithContext,
+ mailboxSession: MailboxSession,
+ request: MDNSendRequest):
SFlux[InvocationWithContext] = {
+ identifyResolver.resolveIdentityId(request.identityId, mailboxSession)
+ .flatMap(maybeIdentity => if (maybeIdentity.isEmpty) {
+ SMono.raiseError(IdentityIdNotFoundException("The IdentityId cannot be
found"))
+ } else {
+ create(maybeIdentity.get, request, mailboxSession,
invocation.processingContext)
+ })
+ .flatMapMany(createdResults => {
+ val explicitInvocation: InvocationWithContext = InvocationWithContext(
+ invocation = Invocation(
+ methodName = invocation.invocation.methodName,
+ arguments =
Arguments(serializer.serializeMDNSendResponse(createdResults._1.asResponse(request.accountId))
+ .as[JsObject]),
+ methodCallId = invocation.invocation.methodCallId),
+ processingContext = createdResults._2)
+
+ val emailSetCall: SMono[InvocationWithContext] =
request.implicitEmailSetRequest(createdResults._1.resolveMessageId)
+ .fold(e => SMono.error(e),
+ maybeEmailSetRequest => maybeEmailSetRequest.map(emailSetRequest
=> emailSetMethod.doProcess(
+ capabilities = capabilities,
+ invocation = invocation,
+ mailboxSession = mailboxSession,
+ request = emailSetRequest))
+ .getOrElse(SMono.empty))
+
+ SFlux.concat(SMono.just(explicitInvocation), emailSetCall)
+ })
+ }
+
+ override def getRequest(mailboxSession: MailboxSession, invocation:
Invocation): Either[Exception, MDNSendRequest] =
+ serializer.deserializeMDNSendRequest(invocation.arguments.value) match {
+ case JsSuccess(mdnSendRequest, _) => mdnSendRequest.validate
+ case errors: JsError => Left(new
IllegalArgumentException(Json.stringify(ResponseSerializer.serialize(errors))))
+ }
+
+ private def create(identity: Identity,
+ request: MDNSendRequest,
+ session: MailboxSession,
+ processingContext: ProcessingContext):
SMono[(MDNSendResults, ProcessingContext)] =
+ SFlux.fromIterable(request.send.view)
+ .fold(MDNSendResults.empty -> processingContext) {
+ (acc: (MDNSendResults, ProcessingContext), elem: (MDNSendCreationId,
JsObject)) => {
+ val (mdnSendId, jsObject) = elem
+ val (creationResult, updatedProcessingContext) =
createMDNSend(session, identity, mdnSendId, jsObject, acc._2)
+ (MDNSendResults.merge(acc._1, creationResult) ->
updatedProcessingContext)
+ }
+ }
+ .subscribeOn(Schedulers.elastic())
+
+ private def createMDNSend(session: MailboxSession,
+ identity: Identity,
+ mdnSendCreationId: MDNSendCreationId,
+ jsObject: JsObject,
+ processingContext: ProcessingContext):
(MDNSendResults, ProcessingContext) =
+ parseMDNRequest(jsObject)
+ .flatMap(createRequest => sendMDN(session, identity, mdnSendCreationId,
createRequest))
+ .fold(error => (MDNSendResults.notSent(mdnSendCreationId, error) ->
processingContext),
+ creation => MDNSendResults.sent(creation) -> processingContext)
+
+ private def parseMDNRequest(jsObject: JsObject):
Either[MDNSendRequestInvalidException, MDNSendCreateRequest] =
+ MDNSendCreateRequest.validateProperties(jsObject)
+ .flatMap(validJson =>
serializer.deserializeMDNSendCreateRequest(validJson) match {
+ case JsSuccess(createRequest, _) => createRequest.validate
+ case JsError(errors) =>
Left(MDNSendRequestInvalidException.parse(errors))
+ })
+
+ private def sendMDN(session: MailboxSession,
+ identity: Identity,
+ mdnSendCreationId: MDNSendCreationId,
+ requestEntry: MDNSendCreateRequest): Either[Throwable,
MDNSendCreateSuccess] =
+ for {
+ mdnRelatedMessageResult <- retrieveRelatedMessageResult(session,
requestEntry)
+ mdnRelatedMessageResultAlready <-
validateMDNNotAlreadySent(mdnRelatedMessageResult)
+ messageRelated = getOriginalMessage(mdnRelatedMessageResultAlready)
+ mailAndResponseAndId <- buildMailAndResponse(identity,
session.getUser.asString(), requestEntry, messageRelated)
+ _ <- Try(queue.enQueue(mailAndResponseAndId._1)).toEither
+ } yield {
+ MDNSendCreateSuccess(
+ mdnCreationId = mdnSendCreationId,
+ createResponse = mailAndResponseAndId._2,
+ forEmailId = mdnRelatedMessageResultAlready.getMessageId)
+ }
+
+ private def retrieveRelatedMessageResult(session: MailboxSession,
requestEntry: MDNSendCreateRequest): Either[MDNSendNotFoundException,
MessageResult] =
+ messageIdManager.getMessage(requestEntry.forEmailId.originalMessageId,
FetchGroup.FULL_CONTENT, session)
+ .asScala
+ .toList
+ .headOption
+ .toRight(MDNSendNotFoundException("The reference \"forEmailId\" cannot
be found."))
+
+
+ private def validateMDNNotAlreadySent(relatedMessageResult: MessageResult):
Either[MDNSendAlreadySentException, MessageResult] =
+ if (relatedMessageResult.getFlags.contains(MDN_ALREADY_SENT_FLAG)) {
+ Left(MDNSendAlreadySentException())
+ } else {
+ scala.Right(relatedMessageResult)
+ }
+
+ private def buildMailAndResponse(identity: Identity, sender: String,
requestEntry: MDNSendCreateRequest, originalMessage: Message):
Either[Exception, (MailImpl, MDNSendCreateResponse)] =
+ for {
+ mailRecipient <- getMailRecipient(originalMessage)
+ mdnFinalRecipient <- getMDNFinalRecipient(requestEntry, identity)
+ mdn = buildMDN(requestEntry, originalMessage, mdnFinalRecipient)
+ subject = buildMessageSubject(requestEntry, originalMessage)
+ (mailImpl, mimeMessage) = buildMailAndMimeMessage(sender, mailRecipient,
subject, mdn)
+ } yield {
+ (mailImpl, buildMDNSendCreateResponse(requestEntry, mdn, mimeMessage))
+ }
+
+ private def buildMailAndMimeMessage(sender: String, recipient: String,
subject: String, mdn: MDN): (MailImpl, MimeMessage) = {
+ val mimeMessage: MimeMessage = mdn.asMimeMessage()
+ mimeMessage.setFrom(sender)
+ mimeMessage.setRecipients(javax.mail.Message.RecipientType.TO, recipient)
+ mimeMessage.setSubject(subject)
+ mimeMessage.saveChanges()
+
+ val mailImpl: MailImpl = MailImpl.builder()
+ .name(MDNId.generate.value)
+ .sender(sender)
+ .addRecipient(recipient)
+ .mimeMessage(mimeMessage)
+ .build()
+ mailImpl -> mimeMessage
+ }
+
+ private def getMailRecipient(originalMessage: Message):
Either[MDNSendNotFoundException, String] =
+ originalMessage.getHeader.getFields(DISPOSITION_NOTIFICATION_TO)
+ .asScala
+ .headOption
+ .map(field => AddressListFieldLenientImpl.PARSER.parse(field, new
DecodeMonitor))
+ .map(addressListField => addressListField.getAddressList)
+ .map(addressList => addressList.flatten())
+ .flatMap(mailboxList => mailboxList.stream().findAny().toScala)
+ .map(mailbox => mailbox.getAddress)
+ .toRight(MDNSendNotFoundException("Invalid
\"Disposition-Notification-To\" header field."))
+
+ private def getMDNFinalRecipient(requestEntry: MDNSendCreateRequest,
identity: Identity): Either[MDNSendForbiddenFromException, FinalRecipient] =
+ requestEntry.finalRecipient
+ .map(finalRecipient => finalRecipient.getMailAddress)
+ .map(mayBeMailAddress => (mayBeMailAddress.isSuccess &&
mayBeMailAddress.get.equals(identity.email)))
+ .map {
+ case true => scala.Right(requestEntry.finalRecipient.get.asJava.get)
+ case false => Left(MDNSendForbiddenFromException("The user is not
allowed to use the given \"finalRecipient\" property"))
+ }
+ .getOrElse(scala.Right(FinalRecipient.builder()
+ .finalRecipient(Text.fromRawText(identity.email.asString()))
+ .build()))
+
+ private def buildMDN(requestEntry: MDNSendCreateRequest, originalMessage:
Message, finalRecipient: FinalRecipient): MDN = {
+ val reportBuilder: MDNReport.Builder = MDNReport.builder()
+ .dispositionField(requestEntry.disposition.asJava.get)
+ .finalRecipientField(finalRecipient)
+ .originalRecipientField(originalMessage.getTo.asScala.head.toString)
+
+ originalMessage.getHeader.getFields("Message-ID")
+ .asScala
+ .map(field => reportBuilder.originalMessageIdField(field.getBody))
+
+ requestEntry.reportingUA
+ .map(uaField => uaField.asJava
+ .map(reportingUserAgent =>
reportBuilder.reportingUserAgentField(reportingUserAgent)))
+
+ requestEntry.extensionFields.map(extensions => extensions
+ .map(extension => reportBuilder.withExtensionField(
+ ExtensionField.builder()
+ .fieldName(extension._1.value)
+ .rawValue(extension._2.value)
+ .build())))
+
+ originalMessage.getHeader.getFields(EmailHeaderName.MESSAGE_ID.value)
+ .asScala
+ .headOption
+ .map(messageIdHeader =>
reportBuilder.originalMessageIdField(TextHeaderValue.from(messageIdHeader).value))
+
+ MDN.builder()
+ .report(reportBuilder.build())
+ .humanReadableText(buildMDNHumanReadableText(requestEntry))
+ .message(requestEntry.includeOriginalMessage
+ .filter(isInclude => isInclude.value)
+ .map(_ => originalMessage)
+ .toJava)
+ .build()
+ }
+
+ private def buildMDNHumanReadableText(requestEntry: MDNSendCreateRequest):
String =
+ requestEntry.textBody.map(textBody => textBody.value)
+ .getOrElse(s"The email has been ${requestEntry.disposition.`type`} on
your recipient's computer")
+
+ private def buildMessageSubject(requestEntry: MDNSendCreateRequest,
originalMessage: Message): String =
+ requestEntry.subject
+ .map(subject => subject.value)
+ .getOrElse(s"""[Received] ${originalMessage.getSubject}""")
+
+ private def buildMDNSendCreateResponse(requestEntry: MDNSendCreateRequest,
mdn: MDN, mimeMessage: MimeMessage): MDNSendCreateResponse =
+ MDNSendCreateResponse(
+ subject = requestEntry.subject match {
+ case Some(_) => None
+ case None => Some(SubjectField(mimeMessage.getSubject))
+ },
+ textBody = requestEntry.textBody match {
+ case Some(_) => None
+ case None => Some(TextBodyField(mdn.getHumanReadableText))
+ },
+ reportingUA = requestEntry.reportingUA match {
+ case Some(_) => None
+ case None => mdn.getReport.getReportingUserAgentField
+ .map(ua => ReportUAField(ua.fieldValue()))
+ .toScala
+ },
+ mdnGateway = mdn.getReport.getGatewayField
+ .map(gateway => MDNGatewayField(gateway.fieldValue()))
+ .toScala,
+ originalRecipient = mdn.getReport.getOriginalRecipientField
+ .map(originalRecipient =>
OriginalRecipientField(originalRecipient.fieldValue()))
+ .toScala,
+ includeOriginalMessage = requestEntry.includeOriginalMessage match {
+ case Some(_) => None
+ case None =>
Some(IncludeOriginalMessageField(mdn.getOriginalMessage.isPresent))
+ },
+ error = Option(mdn.getReport.getErrorFields.asScala
+ .map(error => ErrorField(error.getText.formatted()))
+ .toSeq)
+ .filter(error => error.nonEmpty),
+ finalRecipient = requestEntry.finalRecipient match {
+ case Some(_) => None
+ case None =>
Some(FinalRecipientField(mdn.getReport.getFinalRecipientField.fieldValue()))
+ },
+ originalMessageId = mdn.getReport.getOriginalMessageIdField
+ .map(originalMessageId =>
OriginalMessageIdField(originalMessageId.getOriginalMessageId))
+ .toScala)
+
+ private def getOriginalMessage(messageRelated: MessageResult): Message = {
Review comment:
```suggestion
private def parseAsMimeMessage(messageRelated: MessageResult): Message = {
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDNSend.scala
##########
@@ -0,0 +1,247 @@
+/****************************************************************
+ * 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 cats.implicits.toTraverseOps
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{AccountId, Id, Properties, SetError}
+import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.{JsObject, JsPath, JsonValidationError}
+
+import java.util.UUID
+
+object MDNSend {
+ val MDN_ALREADY_SENT_FLAG: String = "$mdnsent"
+}
+
+object MDNId {
+ def generate: MDNId =
MDNId(Id.validate(UUID.randomUUID().toString).toOption.get)
+}
+
+case class MDNSendCreationId(id: Id)
+
+case class MDNId(value: Id)
+
+object MDNSendRequestInvalidException {
+ def parse(errors: collection.Seq[(JsPath,
collection.Seq[JsonValidationError])]): MDNSendRequestInvalidException = {
+ val setError: SetError = errors.head match {
+ case (path, Seq()) =>
SetError.invalidArguments(SetErrorDescription(s"'$path' property in MDNSend
object is not valid"))
+ case (path, Seq(JsonValidationError(Seq("error.path.missing")))) =>
SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in
MDNSend object"))
+ case (path, Seq(JsonValidationError(Seq(message)))) =>
SetError.invalidArguments(SetErrorDescription(s"'$path' property in MDNSend
object is not valid: $message"))
+ case (path, _) =>
SetError.invalidArguments(SetErrorDescription(s"Unknown error on property
'$path'"))
+ }
+ MDNSendRequestInvalidException(setError)
+ }
+}
+
+case class MDNSendRequestInvalidException(error: SetError) extends Exception
+
+case class MDNSendNotFoundException(description: String) extends Exception
+
+case class MDNSendForbiddenException() extends Exception
+
+case class MDNSendForbiddenFromException(description: String) extends Exception
+
+case class MDNSendOverQuotaException() extends Exception
+
+case class MDNSendTooLargeException() extends Exception
+
+case class MDNSendRateLimitException() extends Exception
+
+case class MDNSendInvalidPropertiesException() extends Exception
+
+case class MDNSendAlreadySentException() extends Exception
+
+case class IdentityIdNotFoundException(description: String) extends Exception
+
+object MDNSendCreateRequest {
+ private val assignableProperties: Set[String] = Set("forEmailId", "subject",
"textBody", "reportingUA",
+ "finalRecipient", "includeOriginalMessage", "disposition",
"extensionFields")
+
+ def validateProperties(jsObject: JsObject):
Either[MDNSendRequestInvalidException, JsObject] =
+ jsObject.keys.diff(assignableProperties) match {
+ case unknownProperties if unknownProperties.nonEmpty =>
+ Left(MDNSendRequestInvalidException(SetError.invalidArguments(
+ SetErrorDescription("Some unknown properties were specified"),
+ Some(Properties.toProperties(unknownProperties.toSet)))))
+ case _ => scala.Right(jsObject)
+ }
+}
+
+case class MDNSendCreateRequest(forEmailId: ForEmailIdField,
+ subject: Option[SubjectField],
+ textBody: Option[TextBodyField],
+ reportingUA: Option[ReportUAField],
+ finalRecipient: Option[FinalRecipientField],
+ includeOriginalMessage:
Option[IncludeOriginalMessageField],
+ disposition: MDNDisposition,
+ extensionFields:
Option[Map[ExtensionFieldName, ExtensionFieldValue]]) {
+ def validate: Either[MDNSendRequestInvalidException, MDNSendCreateRequest] =
+ validateDisposition.flatMap(_ => validateReportUA)
+ .flatMap(_ => validateFinalRecipient)
+
+ def validateDisposition: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ disposition.valid
+ .fold(error => Left(error), _ => scala.Right(this))
+
+ def validateReportUA: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ reportingUA match {
+ case None => scala.Right(this)
+ case Some(value) => value.valid.fold(error => Left(error), _ =>
scala.Right(this))
+ }
+
+ def validateFinalRecipient: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ finalRecipient match {
+ case None => scala.Right(this)
+ case Some(value) => value.valid.fold(error => Left(error), _ =>
scala.Right(this))
+ }
+}
+
+case class MDNSendCreateResponse(subject: Option[SubjectField],
+ textBody: Option[TextBodyField],
+ reportingUA: Option[ReportUAField],
+ mdnGateway: Option[MDNGatewayField],
+ originalRecipient:
Option[OriginalRecipientField],
+ finalRecipient: Option[FinalRecipientField],
+ includeOriginalMessage:
Option[IncludeOriginalMessageField],
+ originalMessageId:
Option[OriginalMessageIdField],
+ error: Option[Seq[ErrorField]])
+
+case class MDNSendRequest(accountId: AccountId,
+ identityId: IdentityId,
+ send: Map[MDNSendCreationId, JsObject],
+ onSuccessUpdateEmail: Option[Map[MDNSendCreationId,
JsObject]]) extends WithAccountId {
+
+ def validate: Either[IllegalArgumentException, MDNSendRequest] = {
+ val supportedCreationIds: List[MDNSendCreationId] = send.keys.toList
+ onSuccessUpdateEmail.getOrElse(Map())
+ .keys
+ .toList
+ .map(id => validateOnSuccessUpdateEmail(id, supportedCreationIds))
+ .sequence
+ .map(_ => this)
+ }
+
+ private def validateOnSuccessUpdateEmail(creationId: MDNSendCreationId,
supportedCreationIds: List[MDNSendCreationId]):
Either[IllegalArgumentException, MDNSendCreationId] =
+ if (creationId.id.value.startsWith("#")) {
+ val realId = creationId.id.value.substring(1)
+ val validateId: Either[IllegalArgumentException, MDNSendCreationId] =
Id.validate(realId).map(id => MDNSendCreationId(id))
+ validateId.flatMap(mdnSendId => if
(supportedCreationIds.contains(mdnSendId)) {
+ scala.Right(mdnSendId)
+ } else {
+ Left(new IllegalArgumentException(s"${creationId.id.value} cannot be
referenced in current method call"))
+ })
+ } else {
+ Left(new IllegalArgumentException(s"${creationId.id.value} cannot be
retrieved as storage for MDNSend is not yet implemented"))
+ }
+
+ def implicitEmailSetRequest(messageIdResolver: MDNSendCreationId =>
Either[IllegalArgumentException, Option[MessageId]]):
Either[IllegalArgumentException, Option[EmailSetRequest]] =
+ resolveOnSuccessUpdateEmail(messageIdResolver)
+ .map(update =>
+ if (update.isEmpty) {
+ None
+ } else {
+ Some(EmailSetRequest(
+ accountId = accountId,
+ create = None,
+ update = update,
+ destroy = None))
+ })
+
+ def resolveOnSuccessUpdateEmail(messageIdResolver: MDNSendCreationId =>
Either[IllegalArgumentException, Option[MessageId]]):
Either[IllegalArgumentException, Option[Map[UnparsedMessageId, JsObject]]] =
+ onSuccessUpdateEmail.map(map => map.toList
+ .map {
+ case (creationId, json) =>
messageIdResolver.apply(creationId).map(msgOpt => msgOpt.map(messageId =>
(EmailSet.asUnparsed(messageId), json)))
+ }
+ .sequence
+ .map(list => list.flatten.toMap))
+ .sequence
+ .map {
+ case Some(value) if value.isEmpty => None
+ case e => e
+ }
+}
+
+case class MDNSendResponse(accountId: AccountId,
+ sent: Option[Map[MDNSendCreationId,
MDNSendCreateResponse]],
+ notSent: Option[Map[MDNSendCreationId, SetError]])
+
+object MDNSendResults {
+ def empty: MDNSendResults = MDNSendResults(None, None, Map.empty)
+
+ def sent(createSuccess: MDNSendCreateSuccess): MDNSendResults =
+ MDNSendResults(sent = Some(Map(createSuccess.mdnCreationId ->
createSuccess.createResponse)),
+ notSent = None,
+ mdnSentIdResolver = Map(createSuccess.mdnCreationId ->
createSuccess.forEmailId))
+
+ def notSent(mdnSendId: MDNSendCreationId, throwable: Throwable):
MDNSendResults = {
+ val setError: SetError = throwable match {
+ case notFound: MDNSendNotFoundException =>
SetError.notFound(SetErrorDescription(notFound.description))
+ case _: MDNSendForbiddenException => SetError(SetError.forbiddenValue,
+ SetErrorDescription("Violate an Access Control List (ACL) or other
permissions policy."),
+ None)
+ case forbiddenFrom: MDNSendForbiddenFromException =>
SetError(SetError.forbiddenFromValue,
+ SetErrorDescription(forbiddenFrom.description),
+ None)
+ case _: MDNSendInvalidPropertiesException =>
SetError(SetError.invalidArgumentValue,
+ SetErrorDescription("The record given is invalid in some way."),
+ None)
+ case _: MDNSendAlreadySentException =>
SetError.mdnAlreadySent(SetErrorDescription("The message has the $mdnsent
keyword already set."))
+ case parseError: MDNSendRequestInvalidException => parseError.error
+ }
+ MDNSendResults(None, Some(Map(mdnSendId -> setError)), Map.empty)
+ }
+
+ def merge(result1: MDNSendResults, result2: MDNSendResults): MDNSendResults
= MDNSendResults(
+ sent = (result1.sent ++ result2.sent).reduceOption((sent1, sent2) => sent1
++ sent2),
+ notSent = (result1.notSent ++ result2.notSent).reduceOption((notSent1,
notSent2) => notSent1 ++ notSent2),
Review comment:
```suggestion
notSent = (result1.notSent ++ result2.notSent).reduceOption(_ ++ _),
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDNSend.scala
##########
@@ -0,0 +1,247 @@
+/****************************************************************
+ * 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 cats.implicits.toTraverseOps
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{AccountId, Id, Properties, SetError}
+import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.{JsObject, JsPath, JsonValidationError}
+
+import java.util.UUID
+
+object MDNSend {
+ val MDN_ALREADY_SENT_FLAG: String = "$mdnsent"
+}
+
+object MDNId {
+ def generate: MDNId =
MDNId(Id.validate(UUID.randomUUID().toString).toOption.get)
+}
+
+case class MDNSendCreationId(id: Id)
+
+case class MDNId(value: Id)
+
+object MDNSendRequestInvalidException {
+ def parse(errors: collection.Seq[(JsPath,
collection.Seq[JsonValidationError])]): MDNSendRequestInvalidException = {
+ val setError: SetError = errors.head match {
+ case (path, Seq()) =>
SetError.invalidArguments(SetErrorDescription(s"'$path' property in MDNSend
object is not valid"))
+ case (path, Seq(JsonValidationError(Seq("error.path.missing")))) =>
SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in
MDNSend object"))
+ case (path, Seq(JsonValidationError(Seq(message)))) =>
SetError.invalidArguments(SetErrorDescription(s"'$path' property in MDNSend
object is not valid: $message"))
+ case (path, _) =>
SetError.invalidArguments(SetErrorDescription(s"Unknown error on property
'$path'"))
+ }
+ MDNSendRequestInvalidException(setError)
+ }
+}
+
+case class MDNSendRequestInvalidException(error: SetError) extends Exception
+
+case class MDNSendNotFoundException(description: String) extends Exception
+
+case class MDNSendForbiddenException() extends Exception
+
+case class MDNSendForbiddenFromException(description: String) extends Exception
+
+case class MDNSendOverQuotaException() extends Exception
+
+case class MDNSendTooLargeException() extends Exception
+
+case class MDNSendRateLimitException() extends Exception
+
+case class MDNSendInvalidPropertiesException() extends Exception
+
+case class MDNSendAlreadySentException() extends Exception
+
+case class IdentityIdNotFoundException(description: String) extends Exception
+
+object MDNSendCreateRequest {
+ private val assignableProperties: Set[String] = Set("forEmailId", "subject",
"textBody", "reportingUA",
+ "finalRecipient", "includeOriginalMessage", "disposition",
"extensionFields")
+
+ def validateProperties(jsObject: JsObject):
Either[MDNSendRequestInvalidException, JsObject] =
+ jsObject.keys.diff(assignableProperties) match {
+ case unknownProperties if unknownProperties.nonEmpty =>
+ Left(MDNSendRequestInvalidException(SetError.invalidArguments(
+ SetErrorDescription("Some unknown properties were specified"),
+ Some(Properties.toProperties(unknownProperties.toSet)))))
+ case _ => scala.Right(jsObject)
+ }
+}
+
+case class MDNSendCreateRequest(forEmailId: ForEmailIdField,
+ subject: Option[SubjectField],
+ textBody: Option[TextBodyField],
+ reportingUA: Option[ReportUAField],
+ finalRecipient: Option[FinalRecipientField],
+ includeOriginalMessage:
Option[IncludeOriginalMessageField],
+ disposition: MDNDisposition,
+ extensionFields:
Option[Map[ExtensionFieldName, ExtensionFieldValue]]) {
+ def validate: Either[MDNSendRequestInvalidException, MDNSendCreateRequest] =
+ validateDisposition.flatMap(_ => validateReportUA)
+ .flatMap(_ => validateFinalRecipient)
+
+ def validateDisposition: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ disposition.valid
+ .fold(error => Left(error), _ => scala.Right(this))
+
+ def validateReportUA: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ reportingUA match {
+ case None => scala.Right(this)
+ case Some(value) => value.valid.fold(error => Left(error), _ =>
scala.Right(this))
+ }
+
+ def validateFinalRecipient: Either[MDNSendRequestInvalidException,
MDNSendCreateRequest] =
+ finalRecipient match {
+ case None => scala.Right(this)
+ case Some(value) => value.valid.fold(error => Left(error), _ =>
scala.Right(this))
+ }
+}
+
+case class MDNSendCreateResponse(subject: Option[SubjectField],
+ textBody: Option[TextBodyField],
+ reportingUA: Option[ReportUAField],
+ mdnGateway: Option[MDNGatewayField],
+ originalRecipient:
Option[OriginalRecipientField],
+ finalRecipient: Option[FinalRecipientField],
+ includeOriginalMessage:
Option[IncludeOriginalMessageField],
+ originalMessageId:
Option[OriginalMessageIdField],
+ error: Option[Seq[ErrorField]])
+
+case class MDNSendRequest(accountId: AccountId,
+ identityId: IdentityId,
+ send: Map[MDNSendCreationId, JsObject],
+ onSuccessUpdateEmail: Option[Map[MDNSendCreationId,
JsObject]]) extends WithAccountId {
+
+ def validate: Either[IllegalArgumentException, MDNSendRequest] = {
+ val supportedCreationIds: List[MDNSendCreationId] = send.keys.toList
+ onSuccessUpdateEmail.getOrElse(Map())
+ .keys
+ .toList
+ .map(id => validateOnSuccessUpdateEmail(id, supportedCreationIds))
+ .sequence
+ .map(_ => this)
+ }
+
+ private def validateOnSuccessUpdateEmail(creationId: MDNSendCreationId,
supportedCreationIds: List[MDNSendCreationId]):
Either[IllegalArgumentException, MDNSendCreationId] =
+ if (creationId.id.value.startsWith("#")) {
+ val realId = creationId.id.value.substring(1)
+ val validateId: Either[IllegalArgumentException, MDNSendCreationId] =
Id.validate(realId).map(id => MDNSendCreationId(id))
+ validateId.flatMap(mdnSendId => if
(supportedCreationIds.contains(mdnSendId)) {
+ scala.Right(mdnSendId)
+ } else {
+ Left(new IllegalArgumentException(s"${creationId.id.value} cannot be
referenced in current method call"))
+ })
+ } else {
+ Left(new IllegalArgumentException(s"${creationId.id.value} cannot be
retrieved as storage for MDNSend is not yet implemented"))
+ }
+
+ def implicitEmailSetRequest(messageIdResolver: MDNSendCreationId =>
Either[IllegalArgumentException, Option[MessageId]]):
Either[IllegalArgumentException, Option[EmailSetRequest]] =
+ resolveOnSuccessUpdateEmail(messageIdResolver)
+ .map(update =>
+ if (update.isEmpty) {
+ None
+ } else {
+ Some(EmailSetRequest(
+ accountId = accountId,
+ create = None,
+ update = update,
+ destroy = None))
+ })
+
+ def resolveOnSuccessUpdateEmail(messageIdResolver: MDNSendCreationId =>
Either[IllegalArgumentException, Option[MessageId]]):
Either[IllegalArgumentException, Option[Map[UnparsedMessageId, JsObject]]] =
+ onSuccessUpdateEmail.map(map => map.toList
+ .map {
+ case (creationId, json) =>
messageIdResolver.apply(creationId).map(msgOpt => msgOpt.map(messageId =>
(EmailSet.asUnparsed(messageId), json)))
+ }
+ .sequence
+ .map(list => list.flatten.toMap))
+ .sequence
+ .map {
+ case Some(value) if value.isEmpty => None
+ case e => e
+ }
+}
+
+case class MDNSendResponse(accountId: AccountId,
+ sent: Option[Map[MDNSendCreationId,
MDNSendCreateResponse]],
+ notSent: Option[Map[MDNSendCreationId, SetError]])
+
+object MDNSendResults {
+ def empty: MDNSendResults = MDNSendResults(None, None, Map.empty)
+
+ def sent(createSuccess: MDNSendCreateSuccess): MDNSendResults =
+ MDNSendResults(sent = Some(Map(createSuccess.mdnCreationId ->
createSuccess.createResponse)),
+ notSent = None,
+ mdnSentIdResolver = Map(createSuccess.mdnCreationId ->
createSuccess.forEmailId))
+
+ def notSent(mdnSendId: MDNSendCreationId, throwable: Throwable):
MDNSendResults = {
+ val setError: SetError = throwable match {
+ case notFound: MDNSendNotFoundException =>
SetError.notFound(SetErrorDescription(notFound.description))
+ case _: MDNSendForbiddenException => SetError(SetError.forbiddenValue,
+ SetErrorDescription("Violate an Access Control List (ACL) or other
permissions policy."),
+ None)
+ case forbiddenFrom: MDNSendForbiddenFromException =>
SetError(SetError.forbiddenFromValue,
+ SetErrorDescription(forbiddenFrom.description),
+ None)
+ case _: MDNSendInvalidPropertiesException =>
SetError(SetError.invalidArgumentValue,
+ SetErrorDescription("The record given is invalid in some way."),
+ None)
+ case _: MDNSendAlreadySentException =>
SetError.mdnAlreadySent(SetErrorDescription("The message has the $mdnsent
keyword already set."))
+ case parseError: MDNSendRequestInvalidException => parseError.error
+ }
+ MDNSendResults(None, Some(Map(mdnSendId -> setError)), Map.empty)
+ }
+
+ def merge(result1: MDNSendResults, result2: MDNSendResults): MDNSendResults
= MDNSendResults(
+ sent = (result1.sent ++ result2.sent).reduceOption((sent1, sent2) => sent1
++ sent2),
Review comment:
```suggestion
sent = (result1.sent ++ result2.sent).reduceOption(_ ++ _),
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDN.scala
##########
@@ -0,0 +1,131 @@
+/****************************************************************
+ * 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.core.MailAddress
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mdn.MDNReportParser
+import org.apache.james.mdn.`type`.DispositionType
+import org.apache.james.mdn.action.mode.DispositionActionMode
+import org.apache.james.mdn.fields.{FinalRecipient, ReportingUserAgent,
Disposition => JavaDisposition}
+import org.apache.james.mdn.sending.mode.DispositionSendingMode
+
+import java.util.Locale
+import scala.util.{Failure, Success, Try}
+
+object MDN {
+ val DISPOSITION_NOTIFICATION_TO: String = "Disposition-Notification-To"
+}
+
+case class MDNDispositionInvalidException(description: String) extends
Exception
+
+case class ForEmailIdField(originalMessageId: MessageId) extends AnyVal
+
+case class SubjectField(value: String) extends AnyVal
+
+case class TextBodyField(value: String) extends AnyVal
+
+case class ReportUAField(value: String) extends AnyVal {
+ def asJava: Try[ReportingUserAgent] = new MDNReportParser("Reporting-UA: " +
value)
+ .reportingUaField
+ .run()
+
+ def valid: Either[MDNSendRequestInvalidException, ReportUAField] =
+ asJava match {
+ case Success(_) => scala.Right(this)
+ case Failure(_) => Left(MDNSendRequestInvalidException(
+ SetError(`type` = SetError.invalidArgumentValue,
+ description = SetErrorDescription("ReportUA can't be parse."),
+ properties = Some(Properties.toProperties(Set("reportingUA"))))))
+ }
+}
+
+case class FinalRecipientField(value: String) extends AnyVal {
+ def asJava: Try[FinalRecipient] = new MDNReportParser("Final-Recipient: " +
value)
+ .finalRecipientField
+ .run()
+
+ def getMailAddress: Try[MailAddress] = Try(new
MailAddress(asJava.get.getFinalRecipient.formatted()))
+
+ def valid: Either[MDNSendRequestInvalidException, FinalRecipientField] =
+ asJava match {
+ case Success(_) => scala.Right(this)
+ case Failure(_) => Left(MDNSendRequestInvalidException(
+ SetError(`type` = SetError.invalidArgumentValue,
+ description = SetErrorDescription("FinalRecipient can't be parse."),
+ properties = Some(Properties.toProperties(Set("finalRecipient"))))))
+ }
+}
+
+case class OriginalRecipientField(value: String) extends AnyVal
+
+case class OriginalMessageIdField(value: String) extends AnyVal
+
+case class ExtensionFieldName(value: String) extends AnyVal
+
+case class ExtensionFieldValue(value: String) extends AnyVal
+
+case class ErrorField(value: String) extends AnyVal
+
+object IncludeOriginalMessageField {
+ def default: IncludeOriginalMessageField = IncludeOriginalMessageField(false)
+}
+
+case class IncludeOriginalMessageField(value: Boolean) extends AnyVal
+
+case class MDNGatewayField(value: String) extends AnyVal
+
+object MDNDisposition {
+ def fromJava(javaDisposition: JavaDisposition): MDNDisposition =
+ MDNDisposition(actionMode = javaDisposition.getActionMode.getValue,
+ sendingMode =
javaDisposition.getSendingMode.getValue.toLowerCase(Locale.US),
+ `type` = javaDisposition.getType.getValue)
+}
+
+case class MDNDisposition(actionMode: String,
+ sendingMode: String,
+ `type`: String) {
+ def asJava: Try[JavaDisposition] =
+ Try(JavaDisposition.builder()
+ .`type`(DispositionType.fromString(`type`)
+ .orElseThrow(() => MDNDispositionInvalidException("Disposition
\"Type\" is invalid.")))
+ .actionMode(DispositionActionMode.fromString(actionMode)
+ .orElseThrow(() => MDNDispositionInvalidException("Disposition
\"ActionMode\" is invalid.")))
+ .sendingMode(DispositionSendingMode.fromString(sendingMode)
+ .orElseThrow(() => MDNDispositionInvalidException("Disposition
\"SendingMode\" is invalid.")))
+ .build())
+
+ def valid: Either[MDNSendRequestInvalidException, MDNDisposition] =
Review comment:
```suggestion
def validate: Either[MDNSendRequestInvalidException, MDNDisposition] =
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDN.scala
##########
@@ -0,0 +1,131 @@
+/****************************************************************
+ * 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.core.MailAddress
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mdn.MDNReportParser
+import org.apache.james.mdn.`type`.DispositionType
+import org.apache.james.mdn.action.mode.DispositionActionMode
+import org.apache.james.mdn.fields.{FinalRecipient, ReportingUserAgent,
Disposition => JavaDisposition}
+import org.apache.james.mdn.sending.mode.DispositionSendingMode
+
+import java.util.Locale
+import scala.util.{Failure, Success, Try}
+
+object MDN {
+ val DISPOSITION_NOTIFICATION_TO: String = "Disposition-Notification-To"
+}
+
+case class MDNDispositionInvalidException(description: String) extends
Exception
+
+case class ForEmailIdField(originalMessageId: MessageId) extends AnyVal
+
+case class SubjectField(value: String) extends AnyVal
+
+case class TextBodyField(value: String) extends AnyVal
+
+case class ReportUAField(value: String) extends AnyVal {
+ def asJava: Try[ReportingUserAgent] = new MDNReportParser("Reporting-UA: " +
value)
+ .reportingUaField
+ .run()
+
+ def valid: Either[MDNSendRequestInvalidException, ReportUAField] =
+ asJava match {
+ case Success(_) => scala.Right(this)
+ case Failure(_) => Left(MDNSendRequestInvalidException(
+ SetError(`type` = SetError.invalidArgumentValue,
+ description = SetErrorDescription("ReportUA can't be parse."),
+ properties = Some(Properties.toProperties(Set("reportingUA"))))))
+ }
+}
+
+case class FinalRecipientField(value: String) extends AnyVal {
+ def asJava: Try[FinalRecipient] = new MDNReportParser("Final-Recipient: " +
value)
+ .finalRecipientField
+ .run()
+
+ def getMailAddress: Try[MailAddress] = Try(new
MailAddress(asJava.get.getFinalRecipient.formatted()))
Review comment:
```suggestion
def getMailAddress: Try[MailAddress] = for {
javaFinalRecipient <- asJava
mailAddress <- new MailAddress(javaFinalRecipient.formatted())
} yield mailAddress
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MDN.scala
##########
@@ -0,0 +1,131 @@
+/****************************************************************
+ * 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.core.MailAddress
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mdn.MDNReportParser
+import org.apache.james.mdn.`type`.DispositionType
+import org.apache.james.mdn.action.mode.DispositionActionMode
+import org.apache.james.mdn.fields.{FinalRecipient, ReportingUserAgent,
Disposition => JavaDisposition}
+import org.apache.james.mdn.sending.mode.DispositionSendingMode
+
+import java.util.Locale
+import scala.util.{Failure, Success, Try}
+
+object MDN {
+ val DISPOSITION_NOTIFICATION_TO: String = "Disposition-Notification-To"
+}
+
+case class MDNDispositionInvalidException(description: String) extends
Exception
+
+case class ForEmailIdField(originalMessageId: MessageId) extends AnyVal
+
+case class SubjectField(value: String) extends AnyVal
+
+case class TextBodyField(value: String) extends AnyVal
+
+case class ReportUAField(value: String) extends AnyVal {
+ def asJava: Try[ReportingUserAgent] = new MDNReportParser("Reporting-UA: " +
value)
+ .reportingUaField
+ .run()
+
+ def valid: Either[MDNSendRequestInvalidException, ReportUAField] =
+ asJava match {
+ case Success(_) => scala.Right(this)
+ case Failure(_) => Left(MDNSendRequestInvalidException(
+ SetError(`type` = SetError.invalidArgumentValue,
+ description = SetErrorDescription("ReportUA can't be parse."),
+ properties = Some(Properties.toProperties(Set("reportingUA"))))))
+ }
+}
+
+case class FinalRecipientField(value: String) extends AnyVal {
+ def asJava: Try[FinalRecipient] = new MDNReportParser("Final-Recipient: " +
value)
+ .finalRecipientField
+ .run()
+
+ def getMailAddress: Try[MailAddress] = Try(new
MailAddress(asJava.get.getFinalRecipient.formatted()))
+
+ def valid: Either[MDNSendRequestInvalidException, FinalRecipientField] =
Review comment:
```suggestion
def validate: Either[MDNSendRequestInvalidException, FinalRecipientField] =
```
##########
File path: mdn/src/main/java/org/apache/james/mdn/MDN.java
##########
@@ -233,6 +238,17 @@ public BodyPart computeReportPart() throws
MessagingException {
return mdnPart;
}
+ public BodyPart computeOriginalMessagePart() throws MessagingException {
Review comment:
```suggestion
public BodyPart computeOriginalMessagePart(Message message) throws
MessagingException {
```
##########
File path:
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentityGetMethod.scala
##########
@@ -66,3 +67,11 @@ class IdentityGetMethod @Inject() (identityFactory:
IdentityFactory,
SMono.fromCallable(() => identityFactory.listIdentities(mailboxSession))
.map(request.computeResponse)
}
+
+case class IdentifyResolver @Inject()(identityFactory: IdentityFactory) {
Review comment:
```suggestion
case class IdentityResolver @Inject()(identityFactory: IdentityFactory) {
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]