JAMES-2346 sendMDN should send a notification back to the original sender
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/9481435b Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/9481435b Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/9481435b Branch: refs/heads/master Commit: 9481435b89ec32e3220202e4e2198e1bbb8b3b73 Parents: 1be875b Author: benwa <btell...@linagora.com> Authored: Thu Mar 8 16:20:15 2018 +0700 Committer: benwa <btell...@linagora.com> Committed: Tue Mar 13 15:11:54 2018 +0700 ---------------------------------------------------------------------- .../java/org/apache/james/mdn/MDNReport.java | 5 + .../methods/integration/SendMDNMethodTest.java | 259 +++++++++++++++++-- .../jmap/methods/MIMEMessageConverter.java | 5 +- .../james/jmap/methods/MessageAppender.java | 27 ++ .../james/jmap/methods/SendMDNProcessor.java | 179 ++++++++++++- .../apache/james/jmap/methods/ValueWithId.java | 6 +- .../org/apache/james/jmap/model/Envelope.java | 31 +++ .../org/apache/james/jmap/model/JmapMDN.java | 148 +++++++++++ .../java/org/apache/james/jmap/model/MDN.java | 148 ----------- .../james/jmap/model/SetMessagesRequest.java | 21 +- .../james/jmap/model/SetMessagesResponse.java | 46 +++- .../apache/james/jmap/model/JmapMDNTest.java | 125 +++++++++ .../org/apache/james/jmap/model/MDNTest.java | 125 --------- .../jmap/model/SetMessagesResponseTest.java | 6 +- 14 files changed, 794 insertions(+), 337 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java ---------------------------------------------------------------------- diff --git a/mdn/src/main/java/org/apache/james/mdn/MDNReport.java b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java index 2878bfa..8785737 100644 --- a/mdn/src/main/java/org/apache/james/mdn/MDNReport.java +++ b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java @@ -57,6 +57,11 @@ public class MDNReport { private ImmutableList.Builder<Error> errorField = ImmutableList.builder(); private ImmutableList.Builder<ExtensionField> extensionFields = ImmutableList.builder(); + public Builder reportingUserAgentField(String userAgentName) { + this.reportingUserAgentField = Optional.of(new ReportingUserAgent(userAgentName, Optional.empty())); + return this; + } + public Builder reportingUserAgentField(String userAgentName, String userAgentProduct) { this.reportingUserAgentField = Optional.of(new ReportingUserAgent(userAgentName, Optional.ofNullable(userAgentProduct))); return this; http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java index 059a7bb..d7690c7 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java @@ -20,26 +20,29 @@ package org.apache.james.jmap.methods.integration; import static com.jayway.restassured.RestAssured.given; +import static com.jayway.restassured.RestAssured.with; import static com.jayway.restassured.config.EncoderConfig.encoderConfig; import static com.jayway.restassured.config.RestAssuredConfig.newConfig; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; import org.apache.http.client.utils.URIBuilder; import org.apache.james.GuiceJamesServer; import org.apache.james.jmap.HttpJmapAuthentication; import org.apache.james.jmap.api.access.AccessToken; import org.apache.james.mailbox.DefaultMailboxes; -import org.apache.james.mailbox.model.MailboxConstants; +import org.apache.james.mailbox.Role; import org.apache.james.mailbox.model.MessageId; import org.apache.james.mailbox.store.probe.MailboxProbe; -import org.apache.james.mdn.action.mode.DispositionActionMode; -import org.apache.james.mdn.sending.mode.DispositionSendingMode; -import org.apache.james.mdn.type.DispositionType; import org.apache.james.modules.MailboxProbeImpl; import org.apache.james.probe.DataProbe; import org.apache.james.utils.DataProbeImpl; @@ -62,7 +65,6 @@ public abstract class SendMDNMethodTest { private static final String PASSWORD = "password"; private static final String BOB_PASSWORD = "bobPassword"; - protected abstract GuiceJamesServer createJmapServer(); protected abstract MessageId randomMessageId(); @@ -70,6 +72,7 @@ public abstract class SendMDNMethodTest { protected abstract void await(); private AccessToken accessToken; + private AccessToken bobAccessToken; private GuiceJamesServer jmapServer; @Before @@ -85,7 +88,6 @@ public abstract class SendMDNMethodTest { .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(StandardCharsets.UTF_8))) .setPort(jmapServer.getProbe(JmapGuiceProbe.class).getJmapPort()) .build(); - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); RestAssured.defaultParser = Parser.JSON; dataProbe.addDomain(USERS_DOMAIN); @@ -93,11 +95,39 @@ public abstract class SendMDNMethodTest { dataProbe.addUser(BOB, BOB_PASSWORD); mailboxProbe.createMailbox("#private", USERNAME, DefaultMailboxes.INBOX); accessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), USERNAME, PASSWORD); + bobAccessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), BOB, BOB_PASSWORD); + await(); + } + + public void sendAnInitialMessage() { + String messageCreationId = "creationId"; + String outboxId = getOutboxId(bobAccessToken); + String requestBody = "[" + + " [" + + " \"setMessages\"," + + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Bob\", \"email\": \"" + BOB + "\"}," + + " \"to\": [{ \"name\": \"User\", \"email\": \"" + USERNAME + "\"}]," + + " \"subject\": \"Message with an attachment\"," + + " \"textBody\": \"Test body, plain text version\"," + + " \"htmlBody\": \"Test <b>body</b>, HTML version\"," + + " \"mailboxIds\": [\"" + outboxId + "\"] " + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + with() + .header("Authorization", bobAccessToken.serialize()) + .body(requestBody) + .post("/jmap") + .then() + .extract() + .body() + .path(ARGUMENTS + ".created." + messageCreationId + ".id"); - mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.OUTBOX); - mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.TRASH); - mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.DRAFTS); - mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.SENT); await(); } @@ -116,20 +146,54 @@ public abstract class SendMDNMethodTest { } @Test - public void sendMDNIsNotSupportedYet() { + public void sendMDNShouldReturnCreatedMessageId() { + sendAnInitialMessage(); + + List<String> messageIds = getMessageIdListForAccount(accessToken.serialize()); + String creationId = "creation-1"; given() .header("Authorization", accessToken.serialize()) .body("[[\"setMessages\", {\"sendMDN\": {" + "\"" + creationId + "\":{" + - " \"messageId\":\"" + randomMessageId().serialize() + "\"," + + " \"messageId\":\"" + messageIds.get(0) + "\"," + + " \"subject\":\"subject\"," + + " \"textBody\":\"textBody\"," + + " \"reportingUA\":\"reportingUA\"," + + " \"disposition\":{" + + " \"actionMode\":\"automatic-action\","+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ + " }" + + "}" + + "}}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".MDNSent." + creationId, notNullValue()); + } + + @Test + public void sendMDNShouldFailOnUnknownMessageId() { + sendAnInitialMessage(); + + String creationId = "creation-1"; + String randomMessageId = randomMessageId().serialize(); + given() + .header("Authorization", accessToken.serialize()) + .body("[[\"setMessages\", {\"sendMDN\": {" + + "\"" + creationId + "\":{" + + " \"messageId\":\"" + randomMessageId + "\"," + " \"subject\":\"subject\"," + " \"textBody\":\"textBody\"," + " \"reportingUA\":\"reportingUA\"," + " \"disposition\":{" + - " \"actionMode\":\"" + DispositionActionMode.Automatic.getValue() + "\","+ - " \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+ - " \"type\":\"" + DispositionType.Processed.getValue() + "\""+ + " \"actionMode\":\"automatic-action\","+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ " }" + "}" + "}}, \"#0\"]]") @@ -141,7 +205,107 @@ public abstract class SendMDNMethodTest { .body(NAME, equalTo("messagesSet")) .body(ARGUMENTS + ".MDNNotSent", hasEntry( equalTo(creationId), - hasEntry("type", "Not implemented yet"))); + hasEntry("type", "invalidArgument"))) + .body(ARGUMENTS + ".MDNNotSent", hasEntry( + equalTo(creationId), + hasEntry("description", "Message with id " + randomMessageId + " not found. Thus could not send MDN."))); + } + + @Test + public void sendMDNShouldSendAMDNBackToTheOriginalMessageAuthor() { + sendAnInitialMessage(); + + List<String> messageIds = getMessageIdListForAccount(accessToken.serialize()); + + // USER sends a MDN back to BOB + String creationId = "creation-1"; + with() + .header("Authorization", accessToken.serialize()) + .body("[[\"setMessages\", {\"sendMDN\": {" + + "\"" + creationId + "\":{" + + " \"messageId\":\"" + messageIds.get(0) + "\"," + + " \"subject\":\"subject\"," + + " \"textBody\":\"Read confirmation\"," + + " \"reportingUA\":\"reportingUA\"," + + " \"disposition\":{" + + " \"actionMode\":\"automatic-action\","+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ + " }" + + "}" + + "}}, \"#0\"]]") + .post("/jmap"); + + await(); + + // BOB should have received it + List<String> bobInboxMessageIds = listMessagesInMailbox(bobAccessToken, getInboxId(bobAccessToken)); + + given() + .header("Authorization", bobAccessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(ARGUMENTS + ".list[0].from.email", is(USERNAME)) + .body(ARGUMENTS + ".list[0].to.email", contains(BOB)) + .body(ARGUMENTS + ".list[0].hasAttachment", is(true)) + .body(ARGUMENTS + ".list[0].textBody", is("Read confirmation")) + .body(ARGUMENTS + ".list[0].subject", is("subject")) + .body(ARGUMENTS + ".list[0].headers.Content-Type", startsWith("multipart/report;")) + .body(ARGUMENTS + ".list[0].attachments[0].type", startsWith("message/disposition-notification")); + } + + @Test + public void sendMDNShouldPositionTheReportAsAnAttachment() { + sendAnInitialMessage(); + + List<String> messageIds = getMessageIdListForAccount(accessToken.serialize()); + + // USER sends a MDN back to BOB + String creationId = "creation-1"; + with() + .header("Authorization", accessToken.serialize()) + .body("[[\"setMessages\", {\"sendMDN\": {" + + "\"" + creationId + "\":{" + + " \"messageId\":\"" + messageIds.get(0) + "\"," + + " \"subject\":\"subject\"," + + " \"textBody\":\"Read confirmation\"," + + " \"reportingUA\":\"reportingUA\"," + + " \"disposition\":{" + + " \"actionMode\":\"automatic-action\","+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ + " }" + + "}" + + "}}, \"#0\"]]") + .post("/jmap"); + + await(); + + // BOB should have received it + List<String> bobInboxMessageIds = listMessagesInMailbox(bobAccessToken, getInboxId(bobAccessToken)); + + String blobId = with() + .header("Authorization", bobAccessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]") + .post("/jmap") + .then() + .extract() + .body() + .path(ARGUMENTS + ".list[0].attachments[0].blobId"); + + given() + .header("Authorization", bobAccessToken.serialize()) + .when() + .get("/download/" + blobId) + .then() + .statusCode(200) + .body(containsString("Reporting-UA: reportingUA;")) + .body(containsString("Final-Recipient: rfc822; usern...@domain.tld")) + .body(containsString("Original-Message-ID: ")) + .body(containsString("Disposition: automatic-action/MDN-sent-automatically;processed")); } @Test @@ -156,9 +320,9 @@ public abstract class SendMDNMethodTest { " \"textBody\":\"textBody\"," + " \"reportingUA\":\"reportingUA\"," + " \"disposition\":{" + - " \"actionMode\":\"" + DispositionActionMode.Automatic.getValue() + "\","+ - " \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+ - " \"type\":\"" + DispositionType.Processed.getValue() + "\""+ + " \"actionMode\":\"automatic-action\","+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ " }" + "}" + "}}, \"#0\"]]") @@ -185,8 +349,8 @@ public abstract class SendMDNMethodTest { " \"textBody\":\"textBody\"," + " \"reportingUA\":\"reportingUA\"," + " \"disposition\":{" + - " \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+ - " \"type\":\"" + DispositionType.Processed.getValue() + "\""+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ " }" + "}" + "}}, \"#0\"]]") @@ -203,7 +367,6 @@ public abstract class SendMDNMethodTest { @Test public void invalidEnumValuesInMDNShouldBeReported() { String creationId = "creation-1"; - // Missing actionMode given() .header("Authorization", accessToken.serialize()) .body("[[\"setMessages\", {\"sendMDN\": {" + @@ -214,8 +377,8 @@ public abstract class SendMDNMethodTest { " \"reportingUA\":\"reportingUA\"," + " \"disposition\":{" + " \"actionMode\":\"invalid\","+ - " \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+ - " \"type\":\"" + DispositionType.Processed.getValue() + "\""+ + " \"sendingMode\":\"MDN-sent-automatically\","+ + " \"type\":\"processed\""+ " }" + "}" + "}}, \"#0\"]]") @@ -229,4 +392,52 @@ public abstract class SendMDNMethodTest { .body(ARGUMENTS + ".description", containsString("Unrecognized MDN Disposition action mode invalid. Should be one of [manual-action, automatic-action]")); } + private String getInboxId(AccessToken accessToken) { + return getMailboxId(accessToken, Role.INBOX); + } + + private String getOutboxId(AccessToken accessToken) { + return getMailboxId(accessToken, Role.OUTBOX); + } + + private String getMailboxId(AccessToken accessToken, Role role) { + return getAllMailboxesIds(accessToken).stream() + .filter(x -> x.get("role").equalsIgnoreCase(role.serialize())) + .map(x -> x.get("id")) + .findFirst().get(); + } + + private List<Map<String, String>> getAllMailboxesIds(AccessToken accessToken) { + return with() + .header("Authorization", accessToken.serialize()) + .body("[[\"getMailboxes\", {\"properties\": [\"role\", \"id\"]}, \"#0\"]]") + .post("/jmap") + .andReturn() + .body() + .jsonPath() + .getList(ARGUMENTS + ".list"); + } + + public List<String> getMessageIdListForAccount(String accessToken) { + return with() + .header("Authorization", accessToken) + .body("[[\"getMessageList\", {}, \"#0\"]]") + .post("/jmap") + .then() + .extract() + .body() + .path(ARGUMENTS + ".messageIds"); + } + + public List<String> listMessagesInMailbox(AccessToken accessToken, String mailboxId) { + return with() + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + mailboxId + "\"]}}, \"#0\"]]") + .post("/jmap") + .then() + .extract() + .body() + .path(ARGUMENTS + ".messageIds"); + } + } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java index 49688b1..a8b1cbf 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java @@ -107,11 +107,14 @@ public class MIMEMessageConverter { } public byte[] convert(ValueWithId.CreationMessageEntry creationMessageEntry, ImmutableList<MessageAttachment> messageAttachments) { + return asBytes(convertToMime(creationMessageEntry, messageAttachments)); + } + public byte[] asBytes(Message message) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); DefaultMessageWriter writer = new DefaultMessageWriter(); try { - writer.writeMessage(convertToMime(creationMessageEntry, messageAttachments), buffer); + writer.writeMessage(message, buffer); } catch (IOException e) { throw Throwables.propagate(e); } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java index c74f59d..409bd91 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java @@ -30,6 +30,7 @@ import javax.mail.util.SharedByteArrayInputStream; import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry; import org.apache.james.jmap.model.Attachment; import org.apache.james.jmap.model.CreationMessage; +import org.apache.james.jmap.model.Keywords; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.mailbox.AttachmentManager; import org.apache.james.mailbox.MailboxManager; @@ -97,6 +98,32 @@ public class MessageAppender { .build(); } + public MessageFactory.MetaDataWithContent appendMessageInMailbox(org.apache.james.mime4j.dom.Message message, + MessageManager messageManager, + List<MessageAttachment> attachments, + Flags flags, + MailboxSession session) throws MailboxException { + + + byte[] messageContent = mimeMessageConverter.asBytes(message); + SharedByteArrayInputStream content = new SharedByteArrayInputStream(messageContent); + Date internalDate = new Date(); + boolean notRecent = false; + + ComposedMessageId appendedMessage = messageManager.appendMessage(content, internalDate, session, notRecent, flags); + + return MessageFactory.MetaDataWithContent.builder() + .uid(appendedMessage.getUid()) + .keywords(Keywords.factory().fromFlags(flags)) + .internalDate(internalDate.toInstant()) + .sharedContent(content) + .size(messageContent.length) + .attachments(attachments) + .mailboxId(messageManager.getId()) + .messageId(appendedMessage.getMessageId()) + .build(); + } + public MessageFactory.MetaDataWithContent appendMessageInMailbox(CreationMessageEntry createdEntry, MailboxId targetMailbox, MailboxSession session) throws MailboxException { http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java index c0dfa27..5f13d44 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java @@ -21,38 +21,195 @@ package org.apache.james.jmap.methods; import static org.apache.james.jmap.methods.Method.JMAP_PREFIX; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + import javax.inject.Inject; +import javax.mail.Flags; +import javax.mail.MessagingException; +import org.apache.james.core.User; +import org.apache.james.jmap.exceptions.MessageNotFoundException; +import org.apache.james.jmap.model.Envelope; +import org.apache.james.jmap.model.JmapMDN; +import org.apache.james.jmap.model.MDNDisposition; +import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.SetError; import org.apache.james.jmap.model.SetMessagesRequest; import org.apache.james.jmap.model.SetMessagesResponse; +import org.apache.james.jmap.utils.SystemMailboxesProvider; import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageIdManager; +import org.apache.james.mailbox.MessageManager; +import org.apache.james.mailbox.Role; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.model.Attachment; +import org.apache.james.mailbox.model.FetchGroupImpl; +import org.apache.james.mailbox.model.MessageAttachment; +import org.apache.james.mailbox.model.MessageId; +import org.apache.james.mailbox.model.MessageResult; +import org.apache.james.mdn.MDN; +import org.apache.james.mdn.MDNReport; +import org.apache.james.mdn.fields.Disposition; 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.dom.field.ParseException; +import org.apache.james.mime4j.message.DefaultMessageBuilder; +import org.apache.james.mime4j.stream.MimeConfig; +import org.apache.james.mime4j.util.MimeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; public class SendMDNProcessor implements SetMessagesProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(SendMDNProcessor.class); + private static final MimeConfig MIME_ENTITY_CONFIG = MimeConfig.custom() + .setMaxContentLen(-1) + .setMaxHeaderCount(-1) + .setMaxHeaderLen(-1) + .setMaxLineLen(-1) + .build(); + private final MetricFactory metricFactory; + private final SystemMailboxesProvider systemMailboxesProvider; + private final MessageIdManager messageIdManager; + private final MessageAppender messageAppender; + private final MessageSender messageSender; @Inject - public SendMDNProcessor(MetricFactory metricFactory) { + public SendMDNProcessor(MetricFactory metricFactory, SystemMailboxesProvider systemMailboxesProvider, + MessageIdManager messageIdManager, MessageAppender messageAppender, MessageSender messageSender) { this.metricFactory = metricFactory; + this.systemMailboxesProvider = systemMailboxesProvider; + this.messageIdManager = messageIdManager; + this.messageAppender = messageAppender; + this.messageSender = messageSender; } @Override public SetMessagesResponse process(SetMessagesRequest request, MailboxSession mailboxSession) { return metricFactory.withMetric(JMAP_PREFIX + "SendMDN", - () -> handleMDNCreation(request)); + () -> handleMDNCreation(request, mailboxSession)); + } + + private SetMessagesResponse handleMDNCreation(SetMessagesRequest request, MailboxSession mailboxSession) { + return request.getSendMDN() + .stream() + .map(MDNCreationEntry -> handleMDNCreation(MDNCreationEntry, mailboxSession)) + .reduce(SetMessagesResponse.builder(), SetMessagesResponse.Builder::mergeWith) + .build(); + } + + private SetMessagesResponse.Builder handleMDNCreation(ValueWithId.MDNCreationEntry MDNCreationEntry, MailboxSession mailboxSession) { + try { + MessageId messageId = sendMdn(MDNCreationEntry, mailboxSession); + return SetMessagesResponse.builder() + .mdnSent(MDNCreationEntry.getCreationId(), messageId); + } catch (MessageNotFoundException e) { + return SetMessagesResponse.builder() + .mdnNotSent(MDNCreationEntry.getCreationId(), + SetError.builder() + .description(String.format("Message with id %s not found. Thus could not send MDN.", + MDNCreationEntry.getValue().getMessageId().serialize())) + .type("invalidArgument") + .build()); + + } catch (Exception e) { + LOGGER.error("Error while sending MDN", e); + return SetMessagesResponse.builder() + .mdnNotSent(MDNCreationEntry.getCreationId(), + SetError.builder() + .description(String.format("Could not send MDN %s", MDNCreationEntry.getCreationId().getId())) + .type("error") + .build()); + } } - public SetMessagesResponse handleMDNCreation(SetMessagesRequest request) { - SetMessagesResponse.Builder builder = SetMessagesResponse.builder(); + private MessageId sendMdn(ValueWithId.MDNCreationEntry MDNCreationEntry, MailboxSession mailboxSession) throws MailboxException, IOException, MessagingException, ParseException, MessageNotFoundException { + JmapMDN mdn = MDNCreationEntry.getValue(); + Message originalMessage = retrieveOriginalMessage(mdn, mailboxSession); + MDNReport mdnReport = generateReport(mdn, originalMessage, mailboxSession); + List<MessageAttachment> reportAsAttachment = ImmutableList.of(convertReportToAttachment(mdnReport)); + User user = User.fromUsername(mailboxSession.getUser().getUserName()); + + Message mdnAnswer = generateMDNMessage(originalMessage, mdn, mdnReport, user); + + Flags seen = new Flags(Flags.Flag.SEEN); + MessageFactory.MetaDataWithContent metaDataWithContent = messageAppender.appendMessageInMailbox(mdnAnswer, + getOutbox(mailboxSession), reportAsAttachment, seen, mailboxSession); - request.getSendMDN() - .forEach(creationMDNEntry -> builder.MDNNotSent(creationMDNEntry.getCreationId(), - SetError.builder() - .description(String.format("Could not send MDN %s", creationMDNEntry.getCreationId().getId())) - .type("Not implemented yet") - .build())); + messageSender.sendMessage(metaDataWithContent, + Envelope.fromMime4JMessage(mdnAnswer), mailboxSession); - return builder.build(); + return metaDataWithContent.getMessageId(); } + + private Message generateMDNMessage(Message originalMessage, JmapMDN mdn, MDNReport mdnReport, User user) throws ParseException, IOException { + return MDN.builder() + .report(mdnReport) + .humanReadableText(mdn.getTextBody()) + .build() + .asMime4JMessageBuilder() + .setTo(originalMessage.getSender().getAddress()) + .setFrom(user.asString()) + .setSubject(mdn.getSubject()) + .setMessageId(MimeUtil.createUniqueMessageId(user.getDomainPart().orElse(null))) + .build(); + } + + private Message retrieveOriginalMessage(JmapMDN mdn, MailboxSession mailboxSession) throws MailboxException, IOException, MessageNotFoundException { + List<MessageResult> messages = messageIdManager.getMessages(ImmutableList.of(mdn.getMessageId()), + FetchGroupImpl.HEADERS, + mailboxSession); + + if (messages.size() == 0) { + throw new MessageNotFoundException(); + } + + DefaultMessageBuilder messageBuilder = new DefaultMessageBuilder(); + messageBuilder.setMimeEntityConfig(MIME_ENTITY_CONFIG); + messageBuilder.setDecodeMonitor(DecodeMonitor.SILENT); + return messageBuilder.parseMessage(messages.get(0).getHeaders().getInputStream()); + } + + private MessageAttachment convertReportToAttachment(MDNReport mdnReport) { + Attachment attachment = Attachment.builder() + .bytes(mdnReport.formattedValue().getBytes(StandardCharsets.UTF_8)) + .type(MDN.DISPOSITION_CONTENT_TYPE) + .build(); + + return MessageAttachment.builder() + .attachment(attachment) + .isInline(true) + .build(); + } + + private MDNReport generateReport(JmapMDN mdn, Message originalMessage, MailboxSession mailboxSession) { + return MDNReport.builder() + .dispositionField(generateDisposition(mdn.getDisposition())) + .originalRecipientField(mailboxSession.getUser().getUserName()) + .originalMessageIdField(originalMessage.getMessageId()) + .finalRecipientField(mailboxSession.getUser().getUserName()) + .reportingUserAgentField(mdn.getReportingUA()) + .build(); + } + + private Disposition generateDisposition(MDNDisposition disposition) { + return Disposition.builder() + .actionMode(disposition.getActionMode()) + .sendingMode(disposition.getSendingMode()) + .type(disposition.getType()) + .build(); + } + + private MessageManager getOutbox(MailboxSession mailboxSession) throws MailboxException { + return systemMailboxesProvider.getMailboxByRole(Role.OUTBOX, mailboxSession) + .findAny() + .orElseThrow(() -> new IllegalStateException("User don't have an Outbox")); + } + } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java index e8ce60c..6304951 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java @@ -21,7 +21,7 @@ package org.apache.james.jmap.methods; import org.apache.james.jmap.model.CreationMessage; import org.apache.james.jmap.model.CreationMessageId; -import org.apache.james.jmap.model.MDN; +import org.apache.james.jmap.model.JmapMDN; import org.apache.james.jmap.model.Message; import org.apache.james.jmap.model.SetError; @@ -51,8 +51,8 @@ public class ValueWithId<T> { } } - public static class CreationMDNEntry extends ValueWithId<MDN> { - public CreationMDNEntry(CreationMessageId creationId, MDN mdn) { + public static class MDNCreationEntry extends ValueWithId<JmapMDN> { + public MDNCreationEntry(CreationMessageId creationId, JmapMDN mdn) { super(creationId, mdn); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java index c4d80f9..28f61c1 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java @@ -21,12 +21,17 @@ package org.apache.james.jmap.model; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import org.apache.james.core.MailAddress; +import org.apache.james.mime4j.dom.address.AddressList; +import org.apache.james.mime4j.dom.address.Mailbox; +import org.apache.james.mime4j.dom.address.MailboxList; import org.apache.james.util.StreamUtils; +import com.github.fge.lambdas.Throwing; import com.github.steveash.guavate.Guavate; import com.google.common.base.Preconditions; @@ -46,11 +51,37 @@ public class Envelope { .collect(Guavate.toImmutableSet())); } + public static Envelope fromMime4JMessage(org.apache.james.mime4j.dom.Message mime4JMessage) { + MailAddress sender = mime4JMessage.getFrom() + .stream() + .findAny() + .map(Mailbox::getAddress) + .map(Throwing.function(MailAddress::new)) + .orElseThrow(() -> new RuntimeException("Sender is mandatory")); + + Stream<MailAddress> to = emailersToMailAddresses(mime4JMessage.getTo()); + Stream<MailAddress> cc = emailersToMailAddresses(mime4JMessage.getCc()); + Stream<MailAddress> bcc = emailersToMailAddresses(mime4JMessage.getBcc()); + + return new Envelope(sender, + StreamUtils.flatten(Stream.of(to, cc, bcc)) + .collect(Guavate.toImmutableSet())); + } + private static Stream<MailAddress> emailersToMailAddresses(List<Emailer> emailers) { return emailers.stream() .map(Emailer::toMailAddress); } + private static Stream<MailAddress> emailersToMailAddresses(AddressList addresses) { + return Optional.ofNullable(addresses) + .map(AddressList::flatten) + .map(MailboxList::stream) + .orElse(Stream.of()) + .map(Mailbox::getAddress) + .map(Throwing.function(MailAddress::new)); + } + private final MailAddress from; private final Set<MailAddress> recipients; http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java new file mode 100644 index 0000000..0e1df56 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java @@ -0,0 +1,148 @@ +/**************************************************************** + * 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.model; + +import java.util.Objects; + +import org.apache.james.mailbox.model.MessageId; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +@JsonDeserialize(builder = JmapMDN.Builder.class) +public class JmapMDN { + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder { + private MessageId messageId; + private String subject; + private String textBody; + private String reportingUA; + private MDNDisposition disposition; + + public Builder messageId(MessageId messageId) { + this.messageId = messageId; + return this; + } + + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + public Builder textBody(String textBody) { + this.textBody = textBody; + return this; + } + + public Builder reportingUA(String reportingUA) { + this.reportingUA = reportingUA; + return this; + } + + public Builder disposition(MDNDisposition disposition) { + this.disposition = disposition; + return this; + } + + public JmapMDN build() { + Preconditions.checkState(messageId != null, "'messageId' is mandatory"); + Preconditions.checkState(subject != null, "'subject' is mandatory"); + Preconditions.checkState(textBody != null, "'textBody' is mandatory"); + Preconditions.checkState(reportingUA != null, "'reportingUA' is mandatory"); + Preconditions.checkState(disposition != null, "'disposition' is mandatory"); + + return new JmapMDN(messageId, subject, textBody, reportingUA, disposition); + } + + } + + private final MessageId messageId; + private final String subject; + private final String textBody; + private final String reportingUA; + private final MDNDisposition disposition; + + @VisibleForTesting + JmapMDN(MessageId messageId, String subject, String textBody, String reportingUA, MDNDisposition disposition) { + this.messageId = messageId; + this.subject = subject; + this.textBody = textBody; + this.reportingUA = reportingUA; + this.disposition = disposition; + } + + public MessageId getMessageId() { + return messageId; + } + + public String getSubject() { + return subject; + } + + public String getTextBody() { + return textBody; + } + + public String getReportingUA() { + return reportingUA; + } + + public MDNDisposition getDisposition() { + return disposition; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof JmapMDN) { + JmapMDN that = (JmapMDN) o; + + return Objects.equals(this.messageId, that.messageId) + && Objects.equals(this.subject, that.subject) + && Objects.equals(this.textBody, that.textBody) + && Objects.equals(this.reportingUA, that.reportingUA) + && Objects.equals(this.disposition, that.disposition); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(messageId, subject, textBody, reportingUA, disposition); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("messageId", messageId) + .add("subject", subject) + .add("textBody", textBody) + .add("reportingUA", reportingUA) + .add("mdnDisposition", disposition) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java deleted file mode 100644 index cf54031..0000000 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java +++ /dev/null @@ -1,148 +0,0 @@ -/**************************************************************** - * 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.model; - -import java.util.Objects; - -import org.apache.james.mailbox.model.MessageId; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; - -@JsonDeserialize(builder = MDN.Builder.class) -public class MDN { - - public static Builder builder() { - return new Builder(); - } - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder { - private MessageId messageId; - private String subject; - private String textBody; - private String reportingUA; - private MDNDisposition disposition; - - public Builder messageId(MessageId messageId) { - this.messageId = messageId; - return this; - } - - public Builder subject(String subject) { - this.subject = subject; - return this; - } - - public Builder textBody(String textBody) { - this.textBody = textBody; - return this; - } - - public Builder reportingUA(String reportingUA) { - this.reportingUA = reportingUA; - return this; - } - - public Builder disposition(MDNDisposition disposition) { - this.disposition = disposition; - return this; - } - - public MDN build() { - Preconditions.checkState(messageId != null, "'messageId' is mandatory"); - Preconditions.checkState(subject != null, "'subject' is mandatory"); - Preconditions.checkState(textBody != null, "'textBody' is mandatory"); - Preconditions.checkState(reportingUA != null, "'reportingUA' is mandatory"); - Preconditions.checkState(disposition != null, "'disposition' is mandatory"); - - return new MDN(messageId, subject, textBody, reportingUA, disposition); - } - - } - - private final MessageId messageId; - private final String subject; - private final String textBody; - private final String reportingUA; - private final MDNDisposition disposition; - - @VisibleForTesting - MDN(MessageId messageId, String subject, String textBody, String reportingUA, MDNDisposition disposition) { - this.messageId = messageId; - this.subject = subject; - this.textBody = textBody; - this.reportingUA = reportingUA; - this.disposition = disposition; - } - - public MessageId getMessageId() { - return messageId; - } - - public String getSubject() { - return subject; - } - - public String getTextBody() { - return textBody; - } - - public String getReportingUA() { - return reportingUA; - } - - public MDNDisposition getDisposition() { - return disposition; - } - - @Override - public final boolean equals(Object o) { - if (o instanceof MDN) { - MDN that = (MDN) o; - - return Objects.equals(this.messageId, that.messageId) - && Objects.equals(this.subject, that.subject) - && Objects.equals(this.textBody, that.textBody) - && Objects.equals(this.reportingUA, that.reportingUA) - && Objects.equals(this.disposition, that.disposition); - } - return false; - } - - @Override - public final int hashCode() { - return Objects.hash(messageId, subject, textBody, reportingUA, disposition); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("messageId", messageId) - .add("subject", subject) - .add("textBody", textBody) - .add("reportingUA", reportingUA) - .add("mdnDisposition", disposition) - .toString(); - } -} http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java index 2f3cd68..ec6b1a8 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java @@ -28,8 +28,8 @@ import java.util.function.Function; import org.apache.commons.lang.NotImplementedException; import org.apache.james.jmap.methods.JmapRequest; import org.apache.james.jmap.methods.UpdateMessagePatchConverter; -import org.apache.james.jmap.methods.ValueWithId.CreationMDNEntry; import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry; +import org.apache.james.jmap.methods.ValueWithId.MDNCreationEntry; import org.apache.james.mailbox.model.MessageId; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -54,7 +54,7 @@ public class SetMessagesRequest implements JmapRequest { private String accountId; private String ifInState; private HashMap<CreationMessageId, CreationMessage> create; - private HashMap<CreationMessageId, MDN> sendMDN; + private HashMap<CreationMessageId, JmapMDN> sendMDN; private ImmutableMap.Builder<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> updatesProvider; private ImmutableList.Builder<MessageId> destroy; @@ -90,12 +90,12 @@ public class SetMessagesRequest implements JmapRequest { return this; } - public Builder sendMDN(CreationMessageId creationMessageId, MDN mdn) { + public Builder sendMDN(CreationMessageId creationMessageId, JmapMDN mdn) { this.sendMDN.put(creationMessageId, mdn); return this; } - public Builder sendMDN(Map<CreationMessageId, MDN> mdns) { + public Builder sendMDN(Map<CreationMessageId, JmapMDN> mdns) { this.sendMDN.putAll(mdns); return this; } @@ -121,9 +121,9 @@ public class SetMessagesRequest implements JmapRequest { .collect(Guavate.toImmutableList()); } - private ImmutableList<CreationMDNEntry> mdnSendings() { + private ImmutableList<MDNCreationEntry> mdnSendings() { return sendMDN.entrySet().stream() - .map(entry -> new CreationMDNEntry(entry.getKey(), entry.getValue())) + .map(entry -> new MDNCreationEntry(entry.getKey(), entry.getValue())) .collect(Guavate.toImmutableList()); } } @@ -131,13 +131,14 @@ public class SetMessagesRequest implements JmapRequest { private final Optional<String> accountId; private final Optional<String> ifInState; private final List<CreationMessageEntry> create; - private final List<CreationMDNEntry> sendMDN; + private final List<MDNCreationEntry> sendMDN; private final Map<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update; private final List<MessageId> destroy; @VisibleForTesting SetMessagesRequest(Optional<String> accountId, Optional<String> ifInState, - List<CreationMessageEntry> create, List<CreationMDNEntry> sendMDN, Map<MessageId, - Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update, List<MessageId> destroy) { + List<CreationMessageEntry> create, List<MDNCreationEntry> sendMDN, + Map<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update, + List<MessageId> destroy) { this.accountId = accountId; this.ifInState = ifInState; this.create = create; @@ -158,7 +159,7 @@ public class SetMessagesRequest implements JmapRequest { return create; } - public List<CreationMDNEntry> getSendMDN() { + public List<MDNCreationEntry> getSendMDN() { return sendMDN; } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java index 5700df0..c8e20e6 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java @@ -51,19 +51,21 @@ public class SetMessagesResponse implements Method.Response { private String oldState; private String newState; private final ImmutableMap.Builder<CreationMessageId, Message> created; + private final ImmutableMap.Builder<CreationMessageId, MessageId> mdnSent; private final ImmutableList.Builder<MessageId> updated; private final ImmutableList.Builder<MessageId> destroyed; private final ImmutableMap.Builder<CreationMessageId, SetError> notCreated; - private final ImmutableMap.Builder<CreationMessageId, SetError> MDNNotSent; + private final ImmutableMap.Builder<CreationMessageId, SetError> mdnNotSent; private final ImmutableMap.Builder<MessageId, SetError> notUpdated; private final ImmutableMap.Builder<MessageId, SetError> notDestroyed; private Builder() { created = ImmutableMap.builder(); + mdnSent = ImmutableMap.builder(); updated = ImmutableList.builder(); destroyed = ImmutableList.builder(); notCreated = ImmutableMap.builder(); - MDNNotSent = ImmutableMap.builder(); + mdnNotSent = ImmutableMap.builder(); notUpdated = ImmutableMap.builder(); notDestroyed = ImmutableMap.builder(); } @@ -90,6 +92,16 @@ public class SetMessagesResponse implements Method.Response { return this; } + public Builder mdnSent(CreationMessageId creationMessageId, MessageId messageId) { + this.mdnSent.put(creationMessageId, messageId); + return this; + } + + public Builder mdnSent(ImmutableMap<CreationMessageId, MessageId> sent) { + this.mdnSent.putAll(sent); + return this; + } + public Builder updated(List<MessageId> updated) { this.updated.addAll(updated); return this; @@ -110,13 +122,13 @@ public class SetMessagesResponse implements Method.Response { return this; } - public Builder MDNNotSent(Map<CreationMessageId, SetError> notCreated) { - this.MDNNotSent.putAll(notCreated); + public Builder mdnNotSent(Map<CreationMessageId, SetError> notCreated) { + this.mdnNotSent.putAll(notCreated); return this; } - public Builder MDNNotSent(CreationMessageId creationMessageId, SetError error) { - this.MDNNotSent.put(creationMessageId, error); + public Builder mdnNotSent(CreationMessageId creationMessageId, SetError error) { + this.mdnNotSent.put(creationMessageId, error); return this; } @@ -146,8 +158,8 @@ public class SetMessagesResponse implements Method.Response { public SetMessagesResponse build() { return new SetMessagesResponse(accountId, oldState, newState, - created.build(), updated.build(), destroyed.build(), - notCreated.build(), MDNNotSent.build(), notUpdated.build(), notDestroyed.build()); + created.build(), mdnSent.build(), updated.build(), destroyed.build(), + notCreated.build(), mdnNotSent.build(), notUpdated.build(), notDestroyed.build()); } } @@ -155,23 +167,25 @@ public class SetMessagesResponse implements Method.Response { private final String oldState; private final String newState; private final ImmutableMap<CreationMessageId, Message> created; + private final ImmutableMap<CreationMessageId, MessageId> mdnSent; private final ImmutableList<MessageId> updated; private final ImmutableList<MessageId> destroyed; private final ImmutableMap<CreationMessageId, SetError> notCreated; - private final ImmutableMap<CreationMessageId, SetError> MDNNotSent; + private final ImmutableMap<CreationMessageId, SetError> mdnNotSent; private final ImmutableMap<MessageId, SetError> notUpdated; private final ImmutableMap<MessageId, SetError> notDestroyed; - @VisibleForTesting SetMessagesResponse(String accountId, String oldState, String newState, ImmutableMap<CreationMessageId, Message> created, ImmutableList<MessageId> updated, ImmutableList<MessageId> destroyed, + @VisibleForTesting SetMessagesResponse(String accountId, String oldState, String newState, ImmutableMap<CreationMessageId, Message> created, ImmutableMap<CreationMessageId, MessageId> mdnSent, ImmutableList<MessageId> updated, ImmutableList<MessageId> destroyed, ImmutableMap<CreationMessageId, SetError> notCreated, ImmutableMap<CreationMessageId, SetError> mdnNotSent, ImmutableMap<MessageId, SetError> notUpdated, ImmutableMap<MessageId, SetError> notDestroyed) { this.accountId = accountId; this.oldState = oldState; this.newState = newState; this.created = created; + this.mdnSent = mdnSent; this.updated = updated; this.destroyed = destroyed; this.notCreated = notCreated; - this.MDNNotSent = mdnNotSent; + this.mdnNotSent = mdnNotSent; this.notUpdated = notUpdated; this.notDestroyed = notDestroyed; } @@ -212,9 +226,14 @@ public class SetMessagesResponse implements Method.Response { return notDestroyed; } + @JsonProperty("MDNSent") + public ImmutableMap<CreationMessageId, MessageId> getMDNSent() { + return mdnSent; + } + @JsonProperty("MDNNotSent") public ImmutableMap<CreationMessageId, SetError> getMDNNotSent() { - return MDNNotSent; + return mdnNotSent; } public SetMessagesResponse.Builder mergeInto(SetMessagesResponse.Builder responseBuilder) { @@ -224,7 +243,8 @@ public class SetMessagesResponse implements Method.Response { responseBuilder.notCreated(getNotCreated()); responseBuilder.notUpdated(getNotUpdated()); responseBuilder.notDestroyed(getNotDestroyed()); - responseBuilder.MDNNotSent(getMDNNotSent()); + responseBuilder.mdnNotSent(getMDNNotSent()); + responseBuilder.mdnSent(getMDNSent()); if (! Strings.isNullOrEmpty(getAccountId())) { responseBuilder.accountId(getAccountId()); } http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java new file mode 100644 index 0000000..5cb045d --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java @@ -0,0 +1,125 @@ +/**************************************************************** + * 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.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.james.mailbox.model.TestMessageId; +import org.apache.james.mdn.action.mode.DispositionActionMode; +import org.apache.james.mdn.sending.mode.DispositionSendingMode; +import org.apache.james.mdn.type.DispositionType; +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class JmapMDNTest { + + public static final String TEXT_BODY = "text body"; + public static final String SUBJECT = "subject"; + public static final String REPORTING_UA = "reportingUA"; + public static final MDNDisposition DISPOSITION = MDNDisposition.builder() + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Automatic) + .type(DispositionType.Processed) + .build(); + public static final TestMessageId MESSAGE_ID = TestMessageId.of(45); + + @Test + public void shouldMatchBeanContract() { + EqualsVerifier.forClass(JmapMDN.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + public void builderShouldReturnObjectWhenAllFieldsAreValid() { + assertThat( + JmapMDN.builder() + .disposition(DISPOSITION) + .messageId(MESSAGE_ID) + .reportingUA(REPORTING_UA) + .subject(SUBJECT) + .textBody(TEXT_BODY) + .build()) + .isEqualTo(new JmapMDN(MESSAGE_ID, SUBJECT, TEXT_BODY, REPORTING_UA, DISPOSITION)); + } + + @Test + public void dispositionIsCompulsory() { + assertThatThrownBy(() -> + JmapMDN.builder() + .messageId(MESSAGE_ID) + .reportingUA(REPORTING_UA) + .subject(SUBJECT) + .textBody(TEXT_BODY) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void messageIdIsCompulsory() { + assertThatThrownBy(() -> + JmapMDN.builder() + .disposition(DISPOSITION) + .reportingUA(REPORTING_UA) + .subject(SUBJECT) + .textBody(TEXT_BODY) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void reportingUAIsCompulsory() { + assertThatThrownBy(() -> + JmapMDN.builder() + .disposition(DISPOSITION) + .messageId(MESSAGE_ID) + .subject(SUBJECT) + .textBody(TEXT_BODY) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void subjectIsCompulsory() { + assertThatThrownBy(() -> + JmapMDN.builder() + .disposition(DISPOSITION) + .messageId(MESSAGE_ID) + .reportingUA(REPORTING_UA) + .textBody(TEXT_BODY) + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void textBodyIsCompulsory() { + assertThatThrownBy(() -> + JmapMDN.builder() + .disposition(DISPOSITION) + .messageId(MESSAGE_ID) + .reportingUA(REPORTING_UA) + .subject(SUBJECT) + .build()) + .isInstanceOf(IllegalStateException.class); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java deleted file mode 100644 index 1527719..0000000 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/**************************************************************** - * 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.model; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.apache.james.mailbox.model.TestMessageId; -import org.apache.james.mdn.action.mode.DispositionActionMode; -import org.apache.james.mdn.sending.mode.DispositionSendingMode; -import org.apache.james.mdn.type.DispositionType; -import org.junit.Test; - -import nl.jqno.equalsverifier.EqualsVerifier; - -public class MDNTest { - - public static final String TEXT_BODY = "text body"; - public static final String SUBJECT = "subject"; - public static final String REPORTING_UA = "reportingUA"; - public static final MDNDisposition DISPOSITION = MDNDisposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .build(); - public static final TestMessageId MESSAGE_ID = TestMessageId.of(45); - - @Test - public void shouldMatchBeanContract() { - EqualsVerifier.forClass(MDN.class) - .allFieldsShouldBeUsed() - .verify(); - } - - @Test - public void builderShouldReturnObjectWhenAllFieldsAreValid() { - assertThat( - MDN.builder() - .disposition(DISPOSITION) - .messageId(MESSAGE_ID) - .reportingUA(REPORTING_UA) - .subject(SUBJECT) - .textBody(TEXT_BODY) - .build()) - .isEqualTo(new MDN(MESSAGE_ID, SUBJECT, TEXT_BODY, REPORTING_UA, DISPOSITION)); - } - - @Test - public void dispositionIsCompulsory() { - assertThatThrownBy(() -> - MDN.builder() - .messageId(MESSAGE_ID) - .reportingUA(REPORTING_UA) - .subject(SUBJECT) - .textBody(TEXT_BODY) - .build()) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void messageIdIsCompulsory() { - assertThatThrownBy(() -> - MDN.builder() - .disposition(DISPOSITION) - .reportingUA(REPORTING_UA) - .subject(SUBJECT) - .textBody(TEXT_BODY) - .build()) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void reportingUAIsCompulsory() { - assertThatThrownBy(() -> - MDN.builder() - .disposition(DISPOSITION) - .messageId(MESSAGE_ID) - .subject(SUBJECT) - .textBody(TEXT_BODY) - .build()) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void subjectIsCompulsory() { - assertThatThrownBy(() -> - MDN.builder() - .disposition(DISPOSITION) - .messageId(MESSAGE_ID) - .reportingUA(REPORTING_UA) - .textBody(TEXT_BODY) - .build()) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void textBodyIsCompulsory() { - assertThatThrownBy(() -> - MDN.builder() - .disposition(DISPOSITION) - .messageId(MESSAGE_ID) - .reportingUA(REPORTING_UA) - .subject(SUBJECT) - .build()) - .isInstanceOf(IllegalStateException.class); - } - -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java index 61cf8fb..4410adc 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java @@ -74,7 +74,8 @@ public class SetMessagesResponseTest { ImmutableMap<MessageId, SetError> notUpdated = ImmutableMap.of(TestMessageId.of(4), SetError.builder().type("updated").build()); ImmutableMap<MessageId, SetError> notDestroyed = ImmutableMap.of(TestMessageId.of(5), SetError.builder().type("destroyed").build()); ImmutableMap<CreationMessageId, SetError> mdnNotSent = ImmutableMap.of(CreationMessageId.of("dead-beef-defec9"), SetError.builder().type("MDNNotSent").build()); - SetMessagesResponse expected = new SetMessagesResponse(null, null, null, created, updated, destroyed, notCreated, mdnNotSent, notUpdated, notDestroyed); + ImmutableMap<CreationMessageId, MessageId> mdnSent = ImmutableMap.of(CreationMessageId.of("dead-beef-defed0"), TestMessageId.of(12)); + SetMessagesResponse expected = new SetMessagesResponse(null, null, null, created, mdnSent, updated, destroyed, notCreated, mdnNotSent, notUpdated, notDestroyed); SetMessagesResponse setMessagesResponse = SetMessagesResponse.builder() .created(created) @@ -83,7 +84,8 @@ public class SetMessagesResponseTest { .notCreated(notCreated) .notUpdated(notUpdated) .notDestroyed(notDestroyed) - .MDNNotSent(mdnNotSent) + .mdnNotSent(mdnNotSent) + .mdnSent(mdnSent) .build(); assertThat(setMessagesResponse).isEqualToComparingFieldByField(expected); --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org