JAMES-1818 Remove store usage in getMessagesMethod by using managers
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/844a7409 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/844a7409 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/844a7409 Branch: refs/heads/master Commit: 844a7409613678c7b3b34b060923d60cd32b0cee Parents: 8c4e86d Author: Raphael Ouazana <[email protected]> Authored: Mon Aug 22 17:38:23 2016 +0200 Committer: Raphael Ouazana <[email protected]> Committed: Mon Aug 29 15:15:44 2016 +0200 ---------------------------------------------------------------------- .../org/apache/james/jmap/JMAPCommonModule.java | 2 + .../integration/SetMessagesMethodTest.java | 1 - .../test/resources/cucumber/GetMessages.feature | 10 +- .../james/jmap/methods/GetMessagesMethod.java | 103 ++++++----- .../jmap/model/MessageContentExtractor.java | 127 ++++++++++++++ .../apache/james/jmap/model/MessageFactory.java | 127 +++++++++++++- .../jmap/methods/GetMessagesMethodTest.java | 25 +-- .../SetMessagesCreationProcessorTest.java | 4 +- .../james/jmap/model/MailboxMessageTest.java | 3 +- .../jmap/model/MessageContentExtractorTest.java | 171 +++++++++++++++++++ .../apache/james/jmap/send/MailFactoryTest.java | 4 +- 11 files changed, 506 insertions(+), 71 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPCommonModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPCommonModule.java b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPCommonModule.java index 77cfe0f..e9d5c6f 100644 --- a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPCommonModule.java +++ b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPCommonModule.java @@ -30,6 +30,7 @@ import org.apache.james.jmap.crypto.JamesSignatureHandler; 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; @@ -63,6 +64,7 @@ public class JMAPCommonModule extends AbstractModule { 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); http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java index c70a7be..7fa3e83 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java @@ -2014,7 +2014,6 @@ public abstract class SetMessagesMethodTest { .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"); http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature index 0cf3fb7..1a8271b 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature @@ -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 @@ -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: @@ -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 |[email protected] | - |header2 |Header2Content | + |From |[email protected] | + |HEADer2 |Header2Content | And the property "date" of the message is null Scenario: Retrieving message should return not found when id does not match http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/GetMessagesMethod.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/GetMessagesMethod.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/GetMessagesMethod.java index 8bc0a3e..1ae5a60 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/GetMessagesMethod.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/GetMessagesMethod.java @@ -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; @@ -38,21 +37,22 @@ import org.apache.james.jmap.model.MessageFactory; 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; @@ -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; } @@ -116,8 +113,8 @@ public class GetMessagesMethod implements Method { 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) @@ -132,41 +129,51 @@ public class GetMessagesMethod implements Method { } - 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(); @@ -174,16 +181,17 @@ public class GetMessagesMethod implements Method { 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; } @@ -199,22 +207,31 @@ public class GetMessagesMethod implements Method { 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; } } } http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageContentExtractor.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageContentExtractor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageContentExtractor.java new file mode 100644 index 0000000..ecb9f48 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageContentExtractor.java @@ -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; + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java index 8790df3..b401b62 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java @@ -18,29 +18,44 @@ ****************************************************************/ package org.apache.james.jmap.model; +import java.io.IOException; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.inject.Inject; +import javax.mail.Flags; +import org.apache.james.jmap.model.MessageContentExtractor.MessageContent; import org.apache.james.jmap.model.message.EMailer; import org.apache.james.jmap.model.message.IndexableMessage; +import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.Cid; +import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MessageAttachment; +import org.apache.james.mailbox.model.MessageResult; import org.apache.james.mailbox.store.extractor.DefaultTextExtractor; import org.apache.james.mailbox.store.mail.model.MailboxMessage; +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.mime4j.message.MessageBuilder; +import org.apache.james.mime4j.stream.Field; import com.github.steveash.guavate.Guavate; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; public class MessageFactory { @@ -48,10 +63,53 @@ public class MessageFactory { public static final ZoneId UTC_ZONE_ID = ZoneId.of("Z"); private final MessagePreviewGenerator messagePreview; + private final MessageContentExtractor messageContentExtractor; @Inject - public MessageFactory(MessagePreviewGenerator messagePreview) { + public MessageFactory(MessagePreviewGenerator messagePreview, MessageContentExtractor messageContentExtractor) { this.messagePreview = messagePreview; + this.messageContentExtractor = messageContentExtractor; + } + + public Message fromMessageResult(MessageResult messageResult, + List<MessageAttachment> attachments, + MailboxId mailboxId, + Function<Long, MessageId> uidToMessageId) throws MailboxException { + MessageId messageId = uidToMessageId.apply(messageResult.getUid()); + + MessageBuilder parsedMessageResult; + MessageContent messageContent; + try { + parsedMessageResult = MessageBuilder.read(messageResult.getFullContent().getInputStream()); + messageContent = messageContentExtractor.extract(parsedMessageResult.build()); + } catch (IOException e) { + throw new MailboxException("Unable to parse message: " + e.getMessage(), e); + } + + return Message.builder() + .id(messageId) + .blobId(BlobId.of(String.valueOf(messageResult.getUid()))) + .threadId(messageId.serialize()) + .mailboxIds(ImmutableList.of(mailboxId.serialize())) + .inReplyToMessageId(getHeader(parsedMessageResult, "in-reply-to")) + .isUnread(! messageResult.getFlags().contains(Flags.Flag.SEEN)) + .isFlagged(messageResult.getFlags().contains(Flags.Flag.FLAGGED)) + .isAnswered(messageResult.getFlags().contains(Flags.Flag.ANSWERED)) + .isDraft(messageResult.getFlags().contains(Flags.Flag.DRAFT)) + .subject(Strings.nullToEmpty(parsedMessageResult.getSubject())) + .headers(toMap(parsedMessageResult.getFields())) + .from(firstFromMailboxList(parsedMessageResult.getFrom())) + .to(fromAddressList(parsedMessageResult.getTo())) + .cc(fromAddressList(parsedMessageResult.getCc())) + .bcc(fromAddressList(parsedMessageResult.getBcc())) + .replyTo(fromAddressList(parsedMessageResult.getReplyTo())) + .size(parsedMessageResult.getSize()) + .date(toZonedDateTime(messageResult.getInternalDate())) + .textBody(messageContent.getTextBody().orElse(null)) + .htmlBody(messageContent.getHtmlBody().orElse(null)) + .preview(getPreview(messageContent)) + .attachments(getAttachments(attachments)) + .build(); } public Message fromMailboxMessage(MailboxMessage mailboxMessage, @@ -86,6 +144,13 @@ public class MessageFactory { .build(); } + private String getPreview(MessageContent messageContent) { + if (messageContent.getHtmlBody().isPresent()) { + return messagePreview.forHTMLBody(messageContent.getHtmlBody()); + } + return messagePreview.forTextBody(messageContent.getTextBody()); + } + private String getPreview(IndexableMessage im) { Optional<String> bodyHtml = im.getBodyHtml(); if (bodyHtml.isPresent()) { @@ -100,6 +165,40 @@ public class MessageFactory { .map(String::trim) .collect(Collectors.joining(MULTIVALUED_HEADERS_SEPARATOR)); } + + private Emailer firstFromMailboxList(MailboxList list) { + if (list == null) { + return null; + } + return list.stream() + .map(this::fromMailbox) + .findFirst() + .orElse(null); + } + + private ImmutableList<Emailer> fromAddressList(AddressList list) { + if (list == null) { + return ImmutableList.of(); + } + return list.flatten() + .stream() + .map(this::fromMailbox) + .collect(Guavate.toImmutableList()); + } + + private Emailer fromMailbox(Mailbox mailbox) { + return Emailer.builder() + .name(getNameOrAddress(mailbox)) + .email(mailbox.getAddress()) + .build(); + } + + private String getNameOrAddress(Mailbox mailbox) { + if (mailbox.getName() != null) { + return mailbox.getName(); + } + return mailbox.getAddress(); + } private Emailer firstElasticSearchEmailers(Set<EMailer> emailers) { return emailers.stream() @@ -129,6 +228,28 @@ public class MessageFactory { .collect(Guavate.toImmutableMap(Map.Entry::getKey, x -> joinOnComma(x.getValue()))); } + private ImmutableMap<String, String> toMap(List<Field> fields) { + Function<Entry<String, Collection<Field>>, String> bodyConcatenator = fieldListEntry -> fieldListEntry.getValue() + .stream() + .map(Field::getBody) + .collect(Collectors.toList()) + .stream() + .collect(Collectors.joining(",")); + return Multimaps.index(fields, Field::getName) + .asMap() + .entrySet() + .stream() + .collect(Guavate.toImmutableMap(Map.Entry::getKey, bodyConcatenator)); + } + + private String getHeader(MessageBuilder message, String header) { + Field field = message.getField(header); + if (field == null) { + return null; + } + return field.getBody(); + } + private String getHeaderAsSingleValue(IndexableMessage im, String header) { return Strings.emptyToNull(joinOnComma(im.getHeaders().get(header))); } @@ -141,6 +262,10 @@ public class MessageFactory { return ZonedDateTime.ofInstant(mailboxMessage.getInternalDate().toInstant(), UTC_ZONE_ID); } + private ZonedDateTime toZonedDateTime(Date date) { + return ZonedDateTime.ofInstant(date.toInstant(), UTC_ZONE_ID); + } + private String getTextBody(IndexableMessage im) { return im.getBodyText().map(Strings::emptyToNull).orElse(null); } http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java index 54463ea..8b2ccdd 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java @@ -37,6 +37,7 @@ import org.apache.james.jmap.model.ClientId; import org.apache.james.jmap.model.GetMessagesRequest; import org.apache.james.jmap.model.GetMessagesResponse; import org.apache.james.jmap.model.Message; +import org.apache.james.jmap.model.MessageContentExtractor; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessageId; import org.apache.james.jmap.model.MessagePreviewGenerator; @@ -102,21 +103,20 @@ public class GetMessagesMethodTest { private static final User ROBERT = new User("robert", "secret"); private StoreMailboxManager mailboxManager; - private InMemoryMailboxSessionMapperFactory mailboxSessionMapperFactory; + private GetMessagesMethod testee; private MailboxSession session; private MailboxPath inboxPath; private ClientId clientId; - - private MessageFactory messageFactory; @Before public void setup() throws MailboxException { clientId = ClientId.of("#0"); - mailboxSessionMapperFactory = new InMemoryMailboxSessionMapperFactory(); + InMemoryMailboxSessionMapperFactory mailboxSessionMapperFactory = new InMemoryMailboxSessionMapperFactory(); HtmlTextExtractor htmlTextExtractor = new MailboxBasedHtmlTextExtractor(new DefaultTextExtractor()); MessagePreviewGenerator messagePreview = new MessagePreviewGenerator(htmlTextExtractor); - messageFactory = new MessageFactory(messagePreview); + MessageContentExtractor messageContentExtractor = new MessageContentExtractor(); + MessageFactory messageFactory = new MessageFactory(messagePreview, messageContentExtractor); MockAuthenticator authenticator = new MockAuthenticator(); authenticator.addUser(ROBERT.username, ROBERT.password); UnionMailboxACLResolver aclResolver = new UnionMailboxACLResolver(); @@ -129,32 +129,29 @@ public class GetMessagesMethodTest { session = mailboxManager.login(ROBERT.username, ROBERT.password, LOGGER); inboxPath = MailboxPath.inbox(session); mailboxManager.createMailbox(inboxPath, session); + testee = new GetMessagesMethod(mailboxManager, messageFactory); } @Test public void processShouldThrowWhenNullRequest() { - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); GetMessagesRequest request = null; assertThatThrownBy(() -> testee.process(request, mock(ClientId.class), mock(MailboxSession.class))).isInstanceOf(NullPointerException.class); } @Test public void processShouldThrowWhenNullSession() { - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); MailboxSession mailboxSession = null; assertThatThrownBy(() -> testee.process(mock(GetMessagesRequest.class), mock(ClientId.class), mailboxSession)).isInstanceOf(NullPointerException.class); } @Test public void processShouldThrowWhenNullClientId() { - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); ClientId clientId = null; assertThatThrownBy(() -> testee.process(mock(GetMessagesRequest.class), clientId, mock(MailboxSession.class))).isInstanceOf(NullPointerException.class); } @Test public void processShouldThrowWhenRequestHasAccountId() { - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); assertThatThrownBy(() -> testee.process( GetMessagesRequest.builder().accountId("abc").build(), mock(ClientId.class), mock(MailboxSession.class))).isInstanceOf(NotImplementedException.class); } @@ -176,7 +173,6 @@ public class GetMessagesMethodTest { new MessageId(ROBERT, inboxPath, message3Uid))) .build(); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result).hasSize(1) @@ -205,7 +201,6 @@ public class GetMessagesMethodTest { .ids(ImmutableList.of(new MessageId(ROBERT, inboxPath, messageUid))) .build(); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result).hasSize(1) @@ -229,7 +224,6 @@ public class GetMessagesMethodTest { .properties(ImmutableList.of()) .build(); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result).hasSize(1) @@ -250,7 +244,6 @@ public class GetMessagesMethodTest { .ids(ImmutableList.of(new MessageId(ROBERT, inboxPath, message1Uid))) .build(); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); Stream<JmapResponse> result = testee.process(request, clientId, session); assertThat(result).hasSize(1) @@ -274,7 +267,6 @@ public class GetMessagesMethodTest { Set<MessageProperty> expected = Sets.newHashSet(MessageProperty.id, MessageProperty.subject); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result).hasSize(1) @@ -298,7 +290,6 @@ public class GetMessagesMethodTest { Set<MessageProperty> expected = Sets.newHashSet(MessageProperty.id, MessageProperty.textBody); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result).hasSize(1) @@ -325,7 +316,6 @@ public class GetMessagesMethodTest { Set<MessageProperty> expected = Sets.newHashSet(MessageProperty.id, MessageProperty.headers); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result) @@ -351,7 +341,6 @@ public class GetMessagesMethodTest { .properties(ImmutableList.of("headers.from", "headers.heADER2")) .build(); - GetMessagesMethod testee = new GetMessagesMethod(mailboxSessionMapperFactory, mailboxSessionMapperFactory, messageFactory); List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList()); assertThat(result) @@ -362,6 +351,6 @@ public class GetMessagesMethodTest { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setFilterProvider(actualFilterProvider.setDefaultFilter(SimpleBeanPropertyFilter.serializeAll())); String response = objectMapper.writer().writeValueAsString(result.get(0)); - assertThat(JsonPath.parse(response).<Map<String, String>>read("$.response.list[0].headers")).containsOnly(MapEntry.entry("from", "[email protected]"), MapEntry.entry("header2", "Header2Content")); + assertThat(JsonPath.parse(response).<Map<String, String>>read("$.response.list[0].headers")).containsOnly(MapEntry.entry("From", "[email protected]"), MapEntry.entry("HEADer2", "Header2Content")); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java index f81ed55..40649f5 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java @@ -43,6 +43,7 @@ import org.apache.james.jmap.model.CreationMessage; import org.apache.james.jmap.model.CreationMessage.DraftEmailer; import org.apache.james.jmap.model.CreationMessageId; import org.apache.james.jmap.model.Message; +import org.apache.james.jmap.model.MessageContentExtractor; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessageId; import org.apache.james.jmap.model.MessagePreviewGenerator; @@ -112,7 +113,8 @@ public class SetMessagesCreationProcessorTest { public void setup() { HtmlTextExtractor htmlTextExtractor = new MailboxBasedHtmlTextExtractor(new DefaultTextExtractor()); MessagePreviewGenerator messagePreview = new MessagePreviewGenerator(htmlTextExtractor); - messageFactory = new MessageFactory(messagePreview); + MessageContentExtractor messageContentExtractor = new MessageContentExtractor(); + messageFactory = new MessageFactory(messagePreview, messageContentExtractor); } private final CreationMessage.Builder creationMessageBuilder = CreationMessage.builder() http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MailboxMessageTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MailboxMessageTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MailboxMessageTest.java index 4ad127c..da0f386 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MailboxMessageTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MailboxMessageTest.java @@ -60,7 +60,8 @@ public class MailboxMessageTest { public void setUp() { htmlTextExtractor = mock(HtmlTextExtractor.class); messagePreview = new MessagePreviewGenerator(htmlTextExtractor); - messageFactory = new MessageFactory(messagePreview); + MessageContentExtractor messageContentExtractor = new MessageContentExtractor(); + messageFactory = new MessageFactory(messagePreview, messageContentExtractor); } @Test(expected=IllegalStateException.class) http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageContentExtractorTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageContentExtractorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageContentExtractorTest.java new file mode 100644 index 0000000..388f115 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageContentExtractorTest.java @@ -0,0 +1,171 @@ +/**************************************************************** + * 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 java.io.IOException; + +import org.apache.james.jmap.model.MessageContentExtractor.MessageContent; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.Multipart; +import org.apache.james.mime4j.message.BasicBodyFactory; +import org.apache.james.mime4j.message.BodyPart; +import org.apache.james.mime4j.message.BodyPartBuilder; +import org.apache.james.mime4j.message.MessageBuilder; +import org.apache.james.mime4j.message.MultipartBuilder; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.base.Charsets; + +public class MessageContentExtractorTest { + private static final String BINARY_CONTENT = "binary"; + private static final String TEXT_CONTENT = "text content"; + private static final String HTML_CONTENT = "<b>html</b> content"; + + private MessageContentExtractor testee; + + private BodyPart htmlPart; + private BodyPart textPart; + + @Before + public void setup() throws IOException { + testee = new MessageContentExtractor(); + textPart = BodyPartBuilder.create().setBody(TEXT_CONTENT, "plain", Charsets.UTF_8).build(); + htmlPart = BodyPartBuilder.create().setBody(HTML_CONTENT, "html", Charsets.UTF_8).build(); + } + + @Test + public void extractShouldReturnEmptyWhenBinaryContentOnly() throws IOException { + Message message = MessageBuilder.create() + .setBody(BasicBodyFactory.INSTANCE.binaryBody(BINARY_CONTENT, Charsets.UTF_8)) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).isEmpty(); + assertThat(actual.getHtmlBody()).isEmpty(); + } + + @Test + public void extractShouldReturnTextOnlyWhenTextOnlyBody() throws IOException { + Message message = MessageBuilder.create() + .setBody(TEXT_CONTENT, Charsets.UTF_8) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).isEmpty(); + } + + @Test + public void extractShouldReturnHtmlOnlyWhenHtmlOnlyBody() throws IOException { + Message message = MessageBuilder.create() + .setBody(HTML_CONTENT, "html", Charsets.UTF_8) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).isEmpty(); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldReturnHtmlAndTextWhenMultipartAlternative() throws IOException { + Multipart multipart = MultipartBuilder.create("alternative") + .addBodyPart(textPart) + .addBodyPart(htmlPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldReturnHtmlWhenMultipartAlternativeWithoutPlainPart() throws IOException { + Multipart multipart = MultipartBuilder.create("alternative") + .addBodyPart(htmlPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).isEmpty(); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldReturnTextWhenMultipartAlternativeWithoutHtmlPart() throws IOException { + Multipart multipart = MultipartBuilder.create("alternative") + .addBodyPart(textPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).isEmpty(); + } + + @Test + public void extractShouldReturnFirstPartOnlyWhenMultipartMixedAndFirstPartIsText() throws IOException { + Multipart multipart = MultipartBuilder.create("mixed") + .addBodyPart(textPart) + .addBodyPart(htmlPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).isEmpty(); + } + + @Test + public void extractShouldReturnFirstPartOnlyWhenMultipartMixedAndFirstPartIsHtml() throws IOException { + Multipart multipart = MultipartBuilder.create("mixed") + .addBodyPart(htmlPart) + .addBodyPart(textPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).isEmpty(); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldReturnHtmlAndTextWhenMultipartMixedAndFirstPartIsMultipartAlternative() throws IOException { + BodyPart multipartAlternative = BodyPartBuilder.create() + .setBody(MultipartBuilder.create("alternative") + .addBodyPart(htmlPart) + .addBodyPart(textPart) + .build()) + .build(); + Multipart multipartMixed = MultipartBuilder.create("mixed") + .addBodyPart(multipartAlternative) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartMixed) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/844a7409/server/protocols/jmap/src/test/java/org/apache/james/jmap/send/MailFactoryTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/send/MailFactoryTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/send/MailFactoryTest.java index f5128a6..a527102 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/send/MailFactoryTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/send/MailFactoryTest.java @@ -27,6 +27,7 @@ import javax.mail.Flags; import javax.mail.util.SharedByteArrayInputStream; import org.apache.james.jmap.model.Message; +import org.apache.james.jmap.model.MessageContentExtractor; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessageId; import org.apache.james.jmap.model.MessagePreviewGenerator; @@ -76,7 +77,8 @@ public class MailFactoryTest { TestId.of(2)); HtmlTextExtractor htmlTextExtractor = new MailboxBasedHtmlTextExtractor(new DefaultTextExtractor()); MessagePreviewGenerator messagePreview = new MessagePreviewGenerator(htmlTextExtractor); - MessageFactory messageFactory = new MessageFactory(messagePreview) ; + MessageContentExtractor messageContentExtractor = new MessageContentExtractor(); + MessageFactory messageFactory = new MessageFactory(messagePreview, messageContentExtractor); jmapMessage = messageFactory.fromMailboxMessage(mailboxMessage, ImmutableList.of(), x -> MessageId.of("test|test|" + x)); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
