Skip to content

Commit

Permalink
JAMES-2361 implement the mailet that refers to the MDN original Jmap Id
Browse files Browse the repository at this point in the history
  • Loading branch information
mbaechler committed Apr 16, 2018
1 parent 8bae242 commit 2387fd8
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 14 deletions.
7 changes: 7 additions & 0 deletions mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java
Expand Up @@ -19,8 +19,11 @@


package org.apache.james.mdn; package org.apache.james.mdn;


import java.io.IOException;
import java.io.InputStream;
import java.util.Optional; import java.util.Optional;


import org.apache.commons.io.IOUtils;
import org.apache.james.mdn.action.mode.DispositionActionMode; import org.apache.james.mdn.action.mode.DispositionActionMode;
import org.apache.james.mdn.fields.AddressType; import org.apache.james.mdn.fields.AddressType;
import org.apache.james.mdn.fields.Disposition; import org.apache.james.mdn.fields.Disposition;
Expand All @@ -46,6 +49,10 @@ public class MDNReportParser {
public 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) { public Optional<MDNReport> parse(String mdnReport) {
Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); Parser parser = Parboiled.createParser(MDNReportParser.Parser.class);
ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionNotificationContent()).run(mdnReport); ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionNotificationContent()).run(mdnReport);
Expand Down
Expand Up @@ -43,6 +43,11 @@
</processor> </processor>


<processor state="transport" enableJmx="false"> <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"> <mailet match="SMTPAuthSuccessful" class="SetMimeHeader">
<name>X-UserIsAuth</name> <name>X-UserIsAuth</name>
<value>true</value> <value>true</value>
Expand All @@ -53,6 +58,7 @@
<mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/> <mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/>
<mailet match="All" class="RecipientRewriteTable" /> <mailet match="All" class="RecipientRewriteTable" />
<mailet match="RecipientIsLocal" class="org.apache.james.jmap.mailet.VacationMailet"/> <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="Sieve"/>
<mailet match="RecipientIsLocal" class="SpamAssassin"> <mailet match="RecipientIsLocal" class="SpamAssassin">
<onMailetException>ignore</onMailetException> <onMailetException>ignore</onMailetException>
Expand Down
Expand Up @@ -63,6 +63,7 @@
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;


import com.google.common.collect.Iterables;
import com.jayway.restassured.RestAssured; import com.jayway.restassured.RestAssured;
import com.jayway.restassured.parsing.Parser; import com.jayway.restassured.parsing.Parser;


Expand Down Expand Up @@ -100,7 +101,7 @@ public void setup() throws Throwable {
bartAccessToken = authenticateJamesUser(baseUri(jmapServer), BART, BOB_PASSWORD); bartAccessToken = authenticateJamesUser(baseUri(jmapServer), BART, BOB_PASSWORD);
} }


private void bartSendMessageToHomer() { private String bartSendMessageToHomer() {
String messageCreationId = "creationId"; String messageCreationId = "creationId";
String outboxId = getOutboxId(bartAccessToken); String outboxId = getOutboxId(bartAccessToken);
String requestBody = "[" + String requestBody = "[" +
Expand All @@ -121,7 +122,7 @@ private void bartSendMessageToHomer() {
" ]" + " ]" +
"]"; "]";


with() String id = with()
.header("Authorization", bartAccessToken.serialize()) .header("Authorization", bartAccessToken.serialize())
.body(requestBody) .body(requestBody)
.post("/jmap") .post("/jmap")
Expand All @@ -131,6 +132,7 @@ private void bartSendMessageToHomer() {
.path(ARGUMENTS + ".created." + messageCreationId + ".id"); .path(ARGUMENTS + ".created." + messageCreationId + ".id");


calmlyAwait.until(() -> !listMessageIdsForAccount(homerAccessToken).isEmpty()); calmlyAwait.until(() -> !listMessageIdsForAccount(homerAccessToken).isEmpty());
return id;
} }


private void sendAWrongInitialMessage() { private void sendAWrongInitialMessage() {
Expand Down Expand Up @@ -276,17 +278,17 @@ public void sendMDNShouldFailOnInvalidMessages() {


@Test @Test
public void sendMDNShouldSendAMDNBackToTheOriginalMessageAuthor() { 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 // HOMER sends a MDN back to BART
String creationId = "creation-1"; String creationId = "creation-1";
with() with()
.header("Authorization", homerAccessToken.serialize()) .header("Authorization", homerAccessToken.serialize())
.body("[[\"setMessages\", {\"sendMDN\": {" + .body("[[\"setMessages\", {\"sendMDN\": {" +
"\"" + creationId + "\":{" + "\"" + creationId + "\":{" +
" \"messageId\":\"" + messageIds.get(0) + "\"," + " \"messageId\":\"" + homerReceivedMessageId + "\"," +
" \"subject\":\"subject\"," + " \"subject\":\"subject\"," +
" \"textBody\":\"Read confirmation\"," + " \"textBody\":\"Read confirmation\"," +
" \"reportingUA\":\"reportingUA\"," + " \"reportingUA\":\"reportingUA\"," +
Expand All @@ -301,22 +303,24 @@ public void sendMDNShouldSendAMDNBackToTheOriginalMessageAuthor() {


// BART should have received it // BART should have received it
calmlyAwait.until(() -> !listMessageIdsInMailbox(bartAccessToken, getInboxId(bartAccessToken)).isEmpty()); 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() given()
.header("Authorization", bartAccessToken.serialize()) .header("Authorization", bartAccessToken.serialize())
.body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]") .body("[[\"getMessages\", {\"ids\": [\"" + bartInboxMessageIds + "\"]}, \"#0\"]]")
.when() .when()
.post("/jmap") .post("/jmap")
.then() .then()
.statusCode(200) .statusCode(200)
.body(ARGUMENTS + ".list[0].from.email", is(HOMER)) .body(firstMessage + ".from.email", is(HOMER))
.body(ARGUMENTS + ".list[0].to.email", contains(BART)) .body(firstMessage + ".to.email", contains(BART))
.body(ARGUMENTS + ".list[0].hasAttachment", is(true)) .body(firstMessage + ".hasAttachment", is(true))
.body(ARGUMENTS + ".list[0].textBody", is("Read confirmation")) .body(firstMessage + ".textBody", is("Read confirmation"))
.body(ARGUMENTS + ".list[0].subject", is("subject")) .body(firstMessage + ".subject", is("subject"))
.body(ARGUMENTS + ".list[0].headers.Content-Type", startsWith("multipart/report;")) .body(firstMessage + ".headers.Content-Type", startsWith("multipart/report;"))
.body(ARGUMENTS + ".list[0].attachments[0].type", startsWith("message/disposition-notification")); .body(firstMessage + ".headers.X-JAMES-MDN-JMAP-MESSAGE-ID", equalTo(bartSentJmapMessageId))
.body(firstMessage + ".attachments[0].type", startsWith("message/disposition-notification"));
} }


@Test @Test
Expand Down
Expand Up @@ -30,6 +30,7 @@
</spooler> </spooler>


<processors> <processors>

<processor state="root" enableJmx="false"> <processor state="root" enableJmx="false">
<mailet match="All" class="PostmasterAlias"/> <mailet match="All" class="PostmasterAlias"/>
<mailet match="RelayLimit=30" class="Null"/> <mailet match="RelayLimit=30" class="Null"/>
Expand All @@ -43,6 +44,11 @@
</processor> </processor>


<processor state="transport" enableJmx="false"> <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"> <mailet match="SMTPAuthSuccessful" class="SetMimeHeader">
<name>X-UserIsAuth</name> <name>X-UserIsAuth</name>
<value>true</value> <value>true</value>
Expand All @@ -53,6 +59,7 @@
<mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/> <mailet match="All" class="org.apache.james.jmap.mailet.TextCalendarBodyToAttachment"/>
<mailet match="All" class="RecipientRewriteTable" /> <mailet match="All" class="RecipientRewriteTable" />
<mailet match="RecipientIsLocal" class="org.apache.james.jmap.mailet.VacationMailet"/> <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="Sieve"/>
<mailet match="RecipientIsLocal" class="SpamAssassin"> <mailet match="RecipientIsLocal" class="SpamAssassin">
<onMailetException>ignore</onMailetException> <onMailetException>ignore</onMailetException>
Expand Down
@@ -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";
}

}

0 comments on commit 2387fd8

Please sign in to comment.