Repository: james-project Updated Branches: refs/heads/master 4e44ec150 -> 2387fd8ee
JAMES-2361 implement the mailet that refers to the MDN original Jmap Id Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/2387fd8e Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/2387fd8e Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/2387fd8e Branch: refs/heads/master Commit: 2387fd8ee22ef5586ff50a194fb8f990b94b8ff1 Parents: 8bae242 Author: Matthieu Baechler <matth...@apache.org> Authored: Fri Apr 6 14:43:46 2018 +0200 Committer: Matthieu Baechler <matth...@apache.org> Committed: Mon Apr 16 15:11:46 2018 +0200 ---------------------------------------------------------------------- .../org/apache/james/mdn/MDNReportParser.java | 7 + .../src/test/resources/mailetcontainer.xml | 6 + .../methods/integration/SendMDNMethodTest.java | 32 ++-- .../src/test/resources/mailetcontainer.xml | 7 + .../mailet/ExtractMDNOriginalJMAPMessageId.java | 165 +++++++++++++++++++ .../ExtractMDNOriginalJMAPMessageIdTest.java | 106 ++++++++++++ 6 files changed, 309 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java ---------------------------------------------------------------------- diff --git a/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java b/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java index d002c87..a1dc416 100644 --- a/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java +++ b/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java @@ -19,8 +19,11 @@ package org.apache.james.mdn; +import java.io.IOException; +import java.io.InputStream; import java.util.Optional; +import org.apache.commons.io.IOUtils; import org.apache.james.mdn.action.mode.DispositionActionMode; import org.apache.james.mdn.fields.AddressType; import org.apache.james.mdn.fields.Disposition; @@ -46,6 +49,10 @@ public class MDNReportParser { public MDNReportParser() { } + public Optional<MDNReport> parse(InputStream is, String charset) throws IOException { + return parse(IOUtils.toString(is, charset)); + } + public Optional<MDNReport> parse(String mdnReport) { Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionNotificationContent()).run(mdnReport); http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/server/protocols/jmap-integration-testing/cassandra-jmap-integration-testing/src/test/resources/mailetcontainer.xml ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/cassandra-jmap-integration-testing/src/test/resources/mailetcontainer.xml b/server/protocols/jmap-integration-testing/cassandra-jmap-integration-testing/src/test/resources/mailetcontainer.xml index 0698316..b9c4b75 100644 --- a/server/protocols/jmap-integration-testing/cassandra-jmap-integration-testing/src/test/resources/mailetcontainer.xml +++ b/server/protocols/jmap-integration-testing/cassandra-jmap-integration-testing/src/test/resources/mailetcontainer.xml @@ -43,6 +43,11 @@ </processor> <processor state="transport" enableJmx="false"> + <matcher name="mdn-matcher" match="org.apache.james.mailetcontainer.impl.matchers.And"> + <matcher match="HasMimeType=multipart/report"/> + <matcher match="HasMimeTypeParameter=report-type=disposition-notification"/> + </matcher> + <mailet match="SMTPAuthSuccessful" class="SetMimeHeader"> <name>X-UserIsAuth</name> <value>true</value> @@ -53,6 +58,7 @@ <mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/> <mailet match="All" class="RecipientRewriteTable" /> <mailet match="RecipientIsLocal" class="org.apache.james.jmap.mailet.VacationMailet"/> + <mailet match="mdn-matcher" class="org.apache.james.jmap.mailet.ExtractMDNOriginalJMAPMessageId" /> <mailet match="RecipientIsLocal" class="Sieve"/> <mailet match="RecipientIsLocal" class="SpamAssassin"> <onMailetException>ignore</onMailetException> http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/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 e283bd4..65422f5 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 @@ -63,6 +63,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import com.google.common.collect.Iterables; import com.jayway.restassured.RestAssured; import com.jayway.restassured.parsing.Parser; @@ -100,7 +101,7 @@ public abstract class SendMDNMethodTest { bartAccessToken = authenticateJamesUser(baseUri(jmapServer), BART, BOB_PASSWORD); } - private void bartSendMessageToHomer() { + private String bartSendMessageToHomer() { String messageCreationId = "creationId"; String outboxId = getOutboxId(bartAccessToken); String requestBody = "[" + @@ -121,7 +122,7 @@ public abstract class SendMDNMethodTest { " ]" + "]"; - with() + String id = with() .header("Authorization", bartAccessToken.serialize()) .body(requestBody) .post("/jmap") @@ -131,6 +132,7 @@ public abstract class SendMDNMethodTest { .path(ARGUMENTS + ".created." + messageCreationId + ".id"); calmlyAwait.until(() -> !listMessageIdsForAccount(homerAccessToken).isEmpty()); + return id; } private void sendAWrongInitialMessage() { @@ -276,9 +278,9 @@ public abstract class SendMDNMethodTest { @Test public void sendMDNShouldSendAMDNBackToTheOriginalMessageAuthor() { - bartSendMessageToHomer(); + String bartSentJmapMessageId = bartSendMessageToHomer(); - List<String> messageIds = listMessageIdsForAccount(homerAccessToken); + String homerReceivedMessageId = Iterables.getOnlyElement(listMessageIdsForAccount(homerAccessToken)); // HOMER sends a MDN back to BART String creationId = "creation-1"; @@ -286,7 +288,7 @@ public abstract class SendMDNMethodTest { .header("Authorization", homerAccessToken.serialize()) .body("[[\"setMessages\", {\"sendMDN\": {" + "\"" + creationId + "\":{" + - " \"messageId\":\"" + messageIds.get(0) + "\"," + + " \"messageId\":\"" + homerReceivedMessageId + "\"," + " \"subject\":\"subject\"," + " \"textBody\":\"Read confirmation\"," + " \"reportingUA\":\"reportingUA\"," + @@ -301,22 +303,24 @@ public abstract class SendMDNMethodTest { // BART should have received it calmlyAwait.until(() -> !listMessageIdsInMailbox(bartAccessToken, getInboxId(bartAccessToken)).isEmpty()); - List<String> bobInboxMessageIds = listMessageIdsInMailbox(bartAccessToken, getInboxId(bartAccessToken)); + String bartInboxMessageIds = Iterables.getOnlyElement(listMessageIdsInMailbox(bartAccessToken, getInboxId(bartAccessToken))); + String firstMessage = ARGUMENTS + ".list[0]"; given() .header("Authorization", bartAccessToken.serialize()) - .body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]") + .body("[[\"getMessages\", {\"ids\": [\"" + bartInboxMessageIds + "\"]}, \"#0\"]]") .when() .post("/jmap") .then() .statusCode(200) - .body(ARGUMENTS + ".list[0].from.email", is(HOMER)) - .body(ARGUMENTS + ".list[0].to.email", contains(BART)) - .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")); + .body(firstMessage + ".from.email", is(HOMER)) + .body(firstMessage + ".to.email", contains(BART)) + .body(firstMessage + ".hasAttachment", is(true)) + .body(firstMessage + ".textBody", is("Read confirmation")) + .body(firstMessage + ".subject", is("subject")) + .body(firstMessage + ".headers.Content-Type", startsWith("multipart/report;")) + .body(firstMessage + ".headers.X-JAMES-MDN-JMAP-MESSAGE-ID", equalTo(bartSentJmapMessageId)) + .body(firstMessage + ".attachments[0].type", startsWith("message/disposition-notification")); } @Test http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/src/test/resources/mailetcontainer.xml ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/src/test/resources/mailetcontainer.xml b/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/src/test/resources/mailetcontainer.xml index fdbe341..878b948 100644 --- a/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/src/test/resources/mailetcontainer.xml +++ b/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/src/test/resources/mailetcontainer.xml @@ -30,6 +30,7 @@ </spooler> <processors> + <processor state="root" enableJmx="false"> <mailet match="All" class="PostmasterAlias"/> <mailet match="RelayLimit=30" class="Null"/> @@ -43,6 +44,11 @@ </processor> <processor state="transport" enableJmx="false"> + <matcher name="mdn-matcher" match="org.apache.james.mailetcontainer.impl.matchers.And"> + <matcher match="HasMimeType=multipart/report"/> + <matcher match="HasMimeTypeParameter=report-type=disposition-notification"/> + </matcher> + <mailet match="SMTPAuthSuccessful" class="SetMimeHeader"> <name>X-UserIsAuth</name> <value>true</value> @@ -53,6 +59,7 @@ <mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/> <mailet match="All" class="RecipientRewriteTable" /> <mailet match="RecipientIsLocal" class="org.apache.james.jmap.mailet.VacationMailet"/> + <mailet match="mdn-matcher" class="org.apache.james.jmap.mailet.ExtractMDNOriginalJMAPMessageId" /> <mailet match="RecipientIsLocal" class="Sieve"/> <mailet match="RecipientIsLocal" class="SpamAssassin"> <onMailetException>ignore</onMailetException> http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java new file mode 100644 index 0000000..06cc2e5 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java @@ -0,0 +1,165 @@ +/**************************************************************** + * 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.mailet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import javax.inject.Inject; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.apache.james.core.MailAddress; +import org.apache.james.mailbox.MailboxManager; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.model.MessageId; +import org.apache.james.mailbox.model.MultimailboxesSearchQuery; +import org.apache.james.mailbox.model.SearchQuery; +import org.apache.james.mdn.MDNReport; +import org.apache.james.mdn.MDNReportParser; +import org.apache.james.mdn.fields.OriginalMessageId; +import org.apache.james.mime4j.dom.Entity; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.Multipart; +import org.apache.james.mime4j.dom.SingleBody; +import org.apache.james.mime4j.message.DefaultMessageBuilder; +import org.apache.mailet.Mail; +import org.apache.mailet.base.GenericMailet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; + +/** + * This mailet handles MDN messages and define a header X-JAMES-MDN-JMAP-MESSAGE-ID referencing + * the original message (by its Jmap Id) asking for the recipient to send an MDN. + */ +public class ExtractMDNOriginalJMAPMessageId extends GenericMailet { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExtractMDNOriginalJMAPMessageId.class); + + private static final String MESSAGE_DISPOSITION_NOTIFICATION = "message/disposition-notification"; + private static final String X_JAMES_MDN_JMAP_MESSAGE_ID = "X-JAMES-MDN-JMAP-MESSAGE-ID"; + + private final MailboxManager mailboxManager; + + @Inject + public ExtractMDNOriginalJMAPMessageId(MailboxManager mailboxManager) { + this.mailboxManager = mailboxManager; + } + + @Override + public void service(Mail mail) throws MessagingException { + if (mail.getRecipients().size() != 1) { + LOGGER.warn("MDN should only be sent to a single recipient"); + return; + } + MailAddress recipient = Iterables.getOnlyElement(mail.getRecipients()); + MimeMessage mimeMessage = mail.getMessage(); + + findReport(mimeMessage) + .flatMap(this::parseReport) + .flatMap(MDNReport::getOriginalMessageIdField) + .map(OriginalMessageId::getOriginalMessageId) + .flatMap(messageId -> findMessageIdForRFC822MessageId(messageId, recipient)) + .ifPresent(messageId -> setJmapMessageIdAsHeader(mimeMessage, messageId)); + } + + private void setJmapMessageIdAsHeader(MimeMessage mimeMessage, MessageId messageId) { + try { + mimeMessage.addHeader(X_JAMES_MDN_JMAP_MESSAGE_ID, messageId.serialize()); + } catch (MessagingException e) { + LOGGER.error("unable to add " + X_JAMES_MDN_JMAP_MESSAGE_ID + " header to message", e); + } + } + + private Optional<MessageId> findMessageIdForRFC822MessageId(String messageId, MailAddress recipient) { + try { + MailboxSession session = mailboxManager.createSystemSession(recipient.asString()); + int limit = 1; + MultimailboxesSearchQuery searchByRFC822MessageId = MultimailboxesSearchQuery + .from(new SearchQuery(SearchQuery.mimeMessageID(messageId))) + .build(); + return mailboxManager.search(searchByRFC822MessageId, session, limit).stream().findFirst(); + } catch (MailboxException e) { + LOGGER.error("unable to find message with Message-Id: " + messageId, e); + } + return Optional.empty(); + } + + private Optional<MDNReport> parseReport(Entity report) { + try { + return new MDNReportParser().parse(((SingleBody)report.getBody()).getInputStream(), report.getCharset()); + } catch (IOException e) { + LOGGER.error("unable to parse MESSAGE_DISPOSITION_NOTIFICATION part", e); + return Optional.empty(); + } + } + + private Optional<Entity> findReport(MimeMessage mimeMessage) { + return parseMessage(mimeMessage).flatMap(this::extractReport); + } + + @VisibleForTesting Optional<Entity> extractReport(Message message) { + if (!message.isMultipart()) { + LOGGER.debug("MDN Message must be multipart"); + return Optional.empty(); + } + List<Entity> bodyParts = ((Multipart) message.getBody()).getBodyParts(); + if (bodyParts.size() < 2) { + LOGGER.debug("MDN Message must contain at least two parts"); + return Optional.empty(); + } + Entity report = bodyParts.get(1); + if (!isDispositionNotification(report)) { + LOGGER.debug("MDN Message second part must be of type " + MESSAGE_DISPOSITION_NOTIFICATION); + return Optional.empty(); + } + return Optional.of(report); + } + + private boolean isDispositionNotification(Entity entity) { + return entity + .getMimeType() + .startsWith(MESSAGE_DISPOSITION_NOTIFICATION); + } + + private Optional<Message> parseMessage(MimeMessage mimeMessage) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + mimeMessage.writeTo(os); + Message message = new DefaultMessageBuilder().parseMessage(new ByteArrayInputStream(os.toByteArray())); + return Optional.of(message); + } catch (IOException | MessagingException e) { + LOGGER.error("unable to parse message", e); + return Optional.empty(); + } + } + + @Override + public String getMailetInfo() { + return "ExtractMDNOriginalJMAPMessageId"; + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/2387fd8e/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageIdTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageIdTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageIdTest.java new file mode 100644 index 0000000..0c7ce1d --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageIdTest.java @@ -0,0 +1,106 @@ +/**************************************************************** + * 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.mailet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.james.mailbox.MailboxManager; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.message.BodyPart; +import org.apache.james.mime4j.message.BodyPartBuilder; +import org.apache.james.mime4j.message.MultipartBuilder; +import org.apache.james.mime4j.message.SingleBodyBuilder; +import org.junit.Test; + +public class ExtractMDNOriginalJMAPMessageIdTest { + + @Test + public void extractReportShouldRejectNonMultipartMessage() throws IOException { + ExtractMDNOriginalJMAPMessageId testee = new ExtractMDNOriginalJMAPMessageId(mock(MailboxManager.class)); + + Message message = Message.Builder.of() + .setBody("content", StandardCharsets.UTF_8) + .build(); + + assertThat(testee.extractReport(message)).isEmpty(); + } + + @Test + public void extractReportShouldRejectMultipartWithSinglePart() throws Exception { + ExtractMDNOriginalJMAPMessageId testee = new ExtractMDNOriginalJMAPMessageId(mock(MailboxManager.class)); + + Message message = Message.Builder.of() + .setBody( + MultipartBuilder.create() + .setSubType("report") + .addTextPart("content", StandardCharsets.UTF_8) + .build()) + .build(); + + assertThat(testee.extractReport(message)).isEmpty(); + } + + @Test + public void extractReportShouldRejectSecondPartWithBadContentType() throws IOException { + ExtractMDNOriginalJMAPMessageId testee = new ExtractMDNOriginalJMAPMessageId(mock(MailboxManager.class)); + + Message message = Message.Builder.of() + .setBody(MultipartBuilder.create() + .setSubType("report") + .addTextPart("first", StandardCharsets.UTF_8) + .addTextPart("second", StandardCharsets.UTF_8) + .build()) + .build(); + + assertThat(testee.extractReport(message)).isEmpty(); + } + + @Test + public void extractReportShouldExtractMDNWhenValidMDN() throws IOException { + ExtractMDNOriginalJMAPMessageId testee = new ExtractMDNOriginalJMAPMessageId(mock(MailboxManager.class)); + + BodyPart mdn = BodyPartBuilder + .create() + .setBody(SingleBodyBuilder.create() + .setText( + "Reporting-UA: linagora.com; Evolution 3.26.5-1+b1 \n" + + "Final-Recipient: rfc822; ho...@linagora.com\n" + + "Original-Message-ID: <1521557867.2614.0.ca...@apache.org>\n" + + "Disposition: manual-action/MDN-sent-manually;displayed\n") + .buildText()) + .setContentType("message/disposition-notification") + .build(); + + Message message = Message.Builder.of() + .setBody(MultipartBuilder.create("report") + .addTextPart("first", StandardCharsets.UTF_8) + .addBodyPart(mdn) + .build()) + .build(); + + assertThat(testee.extractReport(message)) + .isNotEmpty() + .contains(mdn); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org