Skip to content

Commit

Permalink
JAMES-1818 Remove store usage in getMessagesMethod by using managers
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Ouazana committed Aug 29, 2016
1 parent 8c4e86d commit 844a740
Show file tree
Hide file tree
Showing 11 changed files with 506 additions and 71 deletions.
Expand Up @@ -30,6 +30,7 @@
import org.apache.james.jmap.crypto.SignatureHandler;
import org.apache.james.jmap.crypto.SignedTokenFactory;
import org.apache.james.jmap.crypto.SignedTokenManager;
import org.apache.james.jmap.model.MessageContentExtractor;
import org.apache.james.jmap.model.MessageFactory;
import org.apache.james.jmap.model.MessagePreviewGenerator;
import org.apache.james.jmap.send.MailFactory;
Expand Down Expand Up @@ -63,6 +64,7 @@ protected void configure() {
bind(AutomaticallySentMailDetectorImpl.class).in(Scopes.SINGLETON);
bind(MessageFactory.class).in(Scopes.SINGLETON);
bind(MessagePreviewGenerator.class).in(Scopes.SINGLETON);
bind(MessageContentExtractor.class).in(Scopes.SINGLETON);
bind(HeadersAuthenticationExtractor.class).in(Scopes.SINGLETON);
bind(StoreAttachmentManager.class).in(Scopes.SINGLETON);

Expand Down
Expand Up @@ -2014,7 +2014,6 @@ public void attachmentsAndBodysShouldBeRetrievedWhenChainingSetMessagesAndGetMes
.body(firstAttachment + ".size", equalTo((int) attachment.getSize()));
}

@Ignore("We should rework org.apache.james.jmap.model.message.MimePart to handle multipart/alternative and multipart/mixed")
@Test
public void attachmentsAndBodyShouldBeRetrievedWhenChainingSetMessagesAndGetMessagesWithTextBodyAndHtmlAttachment() throws Exception {
jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "sent");
Expand Down
Expand Up @@ -44,7 +44,7 @@ Feature: GetMessages method
And the isUnread of the message is "true"
And the preview of the message is "testmail"
And the headers of the message contains:
|subject |my test subject |
|Subject |my test subject |
And the date of the message is "2014-10-30T14:12:00Z"
And the hasAttachment of the message is "false"
And the list of attachments of the message is empty
Expand All @@ -61,8 +61,8 @@ Feature: GetMessages method
And the isUnread of the message is "true"
And the preview of the message is <preview>
And the headers of the message contains:
|content-type |text/html |
|subject |<subject-header> |
|Content-Type |text/html |
|Subject |<subject-header> |
And the date of the message is "2014-10-30T14:12:00Z"

Examples:
Expand Down Expand Up @@ -113,8 +113,8 @@ Feature: GetMessages method
And the property "isUnread" of the message is null
And the property "preview" of the message is null
And the headers of the message contains:
|from |user@domain.tld |
|header2 |Header2Content |
|From |user@domain.tld |
|HEADer2 |Header2Content |
And the property "date" of the message is null

Scenario: Retrieving message should return not found when id does not match
Expand Down
Expand Up @@ -19,7 +19,6 @@

package org.apache.james.jmap.methods;

import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
Expand All @@ -38,21 +37,22 @@
import org.apache.james.jmap.model.MessageId;
import org.apache.james.jmap.model.MessageProperties;
import org.apache.james.jmap.model.MessageProperties.HeaderProperty;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.FetchGroupImpl;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageAttachment;
import org.apache.james.mailbox.model.MessageRange;
import org.apache.james.mailbox.store.mail.MailboxMapperFactory;
import org.apache.james.mailbox.store.mail.MessageMapper;
import org.apache.james.mailbox.store.mail.MessageMapperFactory;
import org.apache.james.mailbox.store.mail.model.Mailbox;
import org.apache.james.mailbox.store.mail.model.MailboxMessage;
import org.javatuples.Pair;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mailbox.model.MessageResultIterator;
import org.javatuples.Triplet;

import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.functions.ThrowingFunction;
import com.github.steveash.guavate.Guavate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
Expand All @@ -63,17 +63,14 @@ public class GetMessagesMethod implements Method {
public static final String HEADERS_FILTER = "headersFilter";
private static final Method.Request.Name METHOD_NAME = Method.Request.name("getMessages");
private static final Method.Response.Name RESPONSE_NAME = Method.Response.name("messages");
private final MessageMapperFactory messageMapperFactory;
private final MailboxMapperFactory mailboxMapperFactory;
private final MailboxManager mailboxManager;
private final MessageFactory messageFactory;

@Inject
@VisibleForTesting GetMessagesMethod(
MessageMapperFactory messageMapperFactory,
MailboxMapperFactory mailboxMapperFactory,
MailboxManager mailboxManager,
MessageFactory messageFactory) {
this.messageMapperFactory = messageMapperFactory;
this.mailboxMapperFactory = mailboxMapperFactory;
this.mailboxManager = mailboxManager;
this.messageFactory = messageFactory;
}

Expand Down Expand Up @@ -116,8 +113,8 @@ private PropertyFilter buildHeadersPropertyFilter(ImmutableSet<HeaderProperty> h
private GetMessagesResponse getMessagesResponse(MailboxSession mailboxSession, GetMessagesRequest getMessagesRequest) {
getMessagesRequest.getAccountId().ifPresent(GetMessagesMethod::notImplemented);

Function<MessageId, Stream<CompletedMailboxMessage>> loadMessages = loadMessage(mailboxSession);
Function<CompletedMailboxMessage, Message> convertToJmapMessage = toJmapMessage(mailboxSession);
Function<MessageId, Stream<CompletedMessageResult>> loadMessages = loadMessage(mailboxSession);
Function<CompletedMessageResult, Message> convertToJmapMessage = toJmapMessage(mailboxSession);

List<Message> result = getMessagesRequest.getIds().stream()
.flatMap(loadMessages)
Expand All @@ -132,58 +129,69 @@ private static void notImplemented(String input) {
}


private Function<CompletedMailboxMessage, Message> toJmapMessage(MailboxSession mailboxSession) {
return (completedMailboxMessage) -> messageFactory.fromMailboxMessage(
completedMailboxMessage.mailboxMessage,
completedMailboxMessage.attachments,
uid -> new MessageId(mailboxSession.getUser(), completedMailboxMessage.mailboxPath , uid));
private Function<CompletedMessageResult, Message> toJmapMessage(MailboxSession mailboxSession) {
ThrowingFunction<CompletedMessageResult, Message> function = (completedMessageResult) -> messageFactory.fromMessageResult(
completedMessageResult.messageResult,
completedMessageResult.attachments,
completedMessageResult.mailboxId,
uid -> new MessageId(mailboxSession.getUser(), completedMessageResult.mailboxPath , uid));
return Throwing.function(function).sneakyThrow();
}

private Function<MessageId, Stream<CompletedMailboxMessage>>
private Function<MessageId, Stream<CompletedMessageResult>>
loadMessage(MailboxSession mailboxSession) {

return Throwing
.function((MessageId messageId) -> {
MailboxPath mailboxPath = messageId.getMailboxPath();
MessageMapper messageMapper = messageMapperFactory.getMessageMapper(mailboxSession);
Mailbox mailbox = mailboxMapperFactory.getMailboxMapper(mailboxSession).findMailboxByPath(mailboxPath);
return Pair.with(
messageMapper.findInMailbox(mailbox, MessageRange.one(messageId.getUid()), MessageMapper.FetchType.Full, 1),
mailboxPath
MessageManager messageManager = mailboxManager.getMailbox(messageId.getMailboxPath(), mailboxSession);
return Triplet.with(
messageManager.getMessages(messageId.getUidAsRange(), FetchGroupImpl.FULL_CONTENT, mailboxSession),
mailboxPath,
messageManager.getId()
);
})
.andThen(Throwing.function((pair) -> retrieveCompleteMailboxMessages(pair, mailboxSession)));
.andThen(Throwing.function((triplet) -> retrieveCompleteMessageResults(triplet, mailboxSession)));
}

private Stream<CompletedMailboxMessage> retrieveCompleteMailboxMessages(Pair<Iterator<MailboxMessage>, MailboxPath> value, MailboxSession mailboxSession) throws MailboxException {
Iterable<MailboxMessage> iterable = () -> value.getValue0();
Stream<MailboxMessage> targetStream = StreamSupport.stream(iterable.spliterator(), false);
private Stream<CompletedMessageResult> retrieveCompleteMessageResults(Triplet<MessageResultIterator, MailboxPath, MailboxId> value, MailboxSession mailboxSession) throws MailboxException {
Iterable<MessageResult> iterable = () -> value.getValue0();
Stream<MessageResult> targetStream = StreamSupport.stream(iterable.spliterator(), false);

MailboxPath mailboxPath = value.getValue1();
MailboxId mailboxId = value.getValue2();
return targetStream
.map(message -> CompletedMailboxMessage.builder().mailboxMessage(message).attachments(message.getAttachments()))
.map(Throwing.function(this::initializeBuilder).sneakyThrow())
.map(builder -> builder.mailboxId(mailboxId))
.map(builder -> builder.mailboxPath(mailboxPath))
.map(builder -> builder.build());
}

private CompletedMessageResult.Builder initializeBuilder(MessageResult message) throws MailboxException {
return CompletedMessageResult.builder()
.messageResult(message)
.attachments(message.getAttachments());
}

private static class CompletedMailboxMessage {
private static class CompletedMessageResult {

public static Builder builder() {
return new Builder();
}

public static class Builder {

private MailboxMessage mailboxMessage;
private MessageResult messageResult;
private List<MessageAttachment> attachments;
private MailboxPath mailboxPath;
private MailboxId mailboxId;

private Builder() {
}

public Builder mailboxMessage(MailboxMessage mailboxMessage) {
Preconditions.checkArgument(mailboxMessage != null);
this.mailboxMessage = mailboxMessage;
public Builder messageResult(MessageResult messageResult) {
Preconditions.checkArgument(messageResult != null);
this.messageResult = messageResult;
return this;
}

Expand All @@ -199,22 +207,31 @@ public Builder mailboxPath(MailboxPath mailboxPath) {
return this;
}

public CompletedMailboxMessage build() {
Preconditions.checkState(mailboxMessage != null);
public Builder mailboxId(MailboxId mailboxId) {
Preconditions.checkArgument(mailboxId != null);
this.mailboxId = mailboxId;
return this;
}

public CompletedMessageResult build() {
Preconditions.checkState(messageResult != null);
Preconditions.checkState(attachments != null);
Preconditions.checkState(mailboxPath != null);
return new CompletedMailboxMessage(mailboxMessage, attachments, mailboxPath);
Preconditions.checkState(mailboxId != null);
return new CompletedMessageResult(messageResult, attachments, mailboxPath, mailboxId);
}
}

private final MailboxMessage mailboxMessage;
private final MessageResult messageResult;
private final List<MessageAttachment> attachments;
private final MailboxPath mailboxPath;
private final MailboxId mailboxId;

public CompletedMailboxMessage(MailboxMessage mailboxMessage, List<MessageAttachment> attachments, MailboxPath mailboxPath) {
this.mailboxMessage = mailboxMessage;
public CompletedMessageResult(MessageResult messageResult, List<MessageAttachment> attachments, MailboxPath mailboxPath, MailboxId mailboxId) {
this.messageResult = messageResult;
this.attachments = attachments;
this.mailboxPath = mailboxPath;
this.mailboxId = mailboxId;
}
}
}
@@ -0,0 +1,127 @@
/****************************************************************
* 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.io.IOException;
import java.util.List;
import java.util.Optional;

import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.dom.Body;
import org.apache.james.mime4j.dom.Entity;
import org.apache.james.mime4j.dom.Multipart;
import org.apache.james.mime4j.dom.TextBody;

import com.github.fge.lambdas.Throwing;

public class MessageContentExtractor {

public MessageContent extract(org.apache.james.mime4j.dom.Message message) throws IOException {
Body body = message.getBody();
if (body instanceof TextBody) {
return parseTextBody(message, (TextBody)body);
}
if (body instanceof Multipart){
return parseMultipart(message, (Multipart)body);
}
return MessageContent.empty();
}

private MessageContent parseTextBody(Entity entity, TextBody textBody) throws IOException {
String bodyContent = asString(textBody);
if ("text/html".equals(entity.getMimeType())) {
return MessageContent.ofHtmlOnly(bodyContent);
}
return MessageContent.ofTextOnly(bodyContent);
}

private MessageContent parseMultipart(Entity entity, Multipart multipart) throws IOException {
if ("multipart/alternative".equals(entity.getMimeType())) {
return parseMultipartAlternative(multipart);
}
return parseMultipartMixed(multipart);
}

private String asString(TextBody textBody) throws IOException {
return IOUtils.toString(textBody.getInputStream(), textBody.getMimeCharset());
}

private MessageContent parseMultipartMixed(Multipart multipart) throws IOException {
List<Entity> parts = multipart.getBodyParts();
if (! parts.isEmpty()) {
Entity firstPart = parts.get(0);
if (firstPart.getBody() instanceof Multipart && "multipart/alternative".equals(firstPart.getMimeType())) {
return parseMultipartAlternative((Multipart)firstPart.getBody());
} else {
if (firstPart.getBody() instanceof TextBody) {
return parseTextBody(firstPart, (TextBody)firstPart.getBody());
}
}
}
return MessageContent.empty();
}

private MessageContent parseMultipartAlternative(Multipart multipart) throws IOException {
Optional<String> textBody = getFirstMatchingTextBody(multipart, "text/plain");
Optional<String> htmlBody = getFirstMatchingTextBody(multipart, "text/html");
return new MessageContent(textBody, htmlBody);
}

private Optional<String> getFirstMatchingTextBody(Multipart multipart, String mimeType) throws IOException {
return multipart.getBodyParts()
.stream()
.filter(part -> mimeType.equals(part.getMimeType()))
.map(Entity::getBody)
.filter(TextBody.class::isInstance)
.map(TextBody.class::cast)
.findFirst()
.map(Throwing.function(this::asString).sneakyThrow());
}

public static class MessageContent {
private final Optional<String> textBody;
private final Optional<String> htmlBody;

public MessageContent(Optional<String> textBody, Optional<String> htmlBody) {
this.textBody = textBody;
this.htmlBody = htmlBody;
}

public static MessageContent ofTextOnly(String textBody) {
return new MessageContent(Optional.of(textBody), Optional.empty());
}

public static MessageContent ofHtmlOnly(String htmlBody) {
return new MessageContent(Optional.empty(), Optional.of(htmlBody));
}

public static MessageContent empty() {
return new MessageContent(Optional.empty(), Optional.empty());
}

public Optional<String> getTextBody() {
return textBody;
}

public Optional<String> getHtmlBody() {
return htmlBody;
}
}
}

0 comments on commit 844a740

Please sign in to comment.