Repository: james-project Updated Branches: refs/heads/master fc7ded8b7 -> e1c486e24
JAMES-1989 Move MessageContentExtractor to james-server-util-java8 Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/e1c486e2 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/e1c486e2 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/e1c486e2 Branch: refs/heads/master Commit: e1c486e24728a32d78533626a78f78c55c3c92d6 Parents: fc7ded8 Author: Antoine Duprat <adup...@linagora.com> Authored: Thu Apr 6 12:09:15 2017 +0200 Committer: Antoine Duprat <adup...@linagora.com> Committed: Thu Apr 6 12:16:25 2017 +0200 ---------------------------------------------------------------------- .../org/apache/james/jmap/JMAPCommonModule.java | 2 +- server/container/util-java8/pom.xml | 16 + .../util/mime/MessageContentExtractor.java | 226 +++++++++ .../util/mime/MessageContentExtractorTest.java | 491 +++++++++++++++++++ .../jmap/model/MessageContentExtractor.java | 226 --------- .../apache/james/jmap/model/MessageFactory.java | 3 +- .../jmap/methods/GetMessagesMethodTest.java | 2 +- .../SetMessagesCreationProcessorTest.java | 2 +- .../jmap/model/MessageContentExtractorTest.java | 490 ------------------ .../james/jmap/model/MessageFactoryTest.java | 1 + .../apache/james/jmap/send/MailFactoryTest.java | 2 +- 11 files changed, 740 insertions(+), 721 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java index c9d7678..565301f 100644 --- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java +++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java @@ -31,7 +31,6 @@ 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.MailboxFactory; -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; @@ -39,6 +38,7 @@ import org.apache.james.jmap.send.MailSpool; import org.apache.james.jmap.utils.HeadersAuthenticationExtractor; import org.apache.james.util.date.DefaultZonedDateTimeProvider; import org.apache.james.util.date.ZonedDateTimeProvider; +import org.apache.james.util.mime.MessageContentExtractor; import org.apache.mailet.base.AutomaticallySentMailDetector; import org.apache.mailet.base.AutomaticallySentMailDetectorImpl; http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/server/container/util-java8/pom.xml ---------------------------------------------------------------------- diff --git a/server/container/util-java8/pom.xml b/server/container/util-java8/pom.xml index a9a455c..3640006 100644 --- a/server/container/util-java8/pom.xml +++ b/server/container/util-java8/pom.xml @@ -127,6 +127,14 @@ </activation> <dependencies> <dependency> + <groupId>org.apache.james</groupId> + <artifactId>apache-mime4j-dom</artifactId> + </dependency> + <dependency> + <groupId>com.github.fge</groupId> + <artifactId>throwing-lambdas</artifactId> + </dependency> + <dependency> <groupId>com.github.steveash.guavate</groupId> <artifactId>guavate</artifactId> </dependency> @@ -135,6 +143,14 @@ <artifactId>guava</artifactId> </dependency> <dependency> + <groupId>com.sun.mail</groupId> + <artifactId>javax.mail</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> </dependency> http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/server/container/util-java8/src/main/java/org/apache/james/util/mime/MessageContentExtractor.java ---------------------------------------------------------------------- diff --git a/server/container/util-java8/src/main/java/org/apache/james/util/mime/MessageContentExtractor.java b/server/container/util-java8/src/main/java/org/apache/james/util/mime/MessageContentExtractor.java new file mode 100644 index 0000000..7f819aa --- /dev/null +++ b/server/container/util-java8/src/main/java/org/apache/james/util/mime/MessageContentExtractor.java @@ -0,0 +1,226 @@ +/**************************************************************** + * 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.util.mime; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.mail.internet.MimeMessage; + +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; +import com.github.fge.lambdas.functions.ThrowingFunction; +import com.google.common.base.Charsets; + +public class MessageContentExtractor { + + public static final String CONTENT_ID = "Content-ID"; + public static final String MULTIPART_ALTERNATIVE = "multipart/alternative"; + public static final String TEXT_HTML = "text/html"; + public static final String TEXT_PLAIN = "text/plain"; + + 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 { + Optional<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 { + MessageContent messageContent = parseMultipartContent(entity, multipart); + if (!messageContent.isEmpty()) { + return messageContent; + } + return parseFirstFoundMultipart(multipart); + } + + private MessageContent parseMultipartContent(Entity entity, Multipart multipart) throws IOException { + switch(entity.getMimeType()) { + case MULTIPART_ALTERNATIVE: + return retrieveHtmlAndPlainTextContent(multipart); + default: + return retrieveFirstReadablePart(multipart); + } + } + + private MessageContent parseFirstFoundMultipart(Multipart multipart) throws IOException { + ThrowingFunction<Entity, MessageContent> parseMultipart = firstPart -> parseMultipart(firstPart, (Multipart)firstPart.getBody()); + return multipart.getBodyParts() + .stream() + .filter(part -> part.getBody() instanceof Multipart) + .findFirst() + .map(Throwing.function(parseMultipart).sneakyThrow()) + .orElse(MessageContent.empty()); + } + + private Optional<String> asString(TextBody textBody) throws IOException { + return Optional.ofNullable(IOUtils.toString(textBody.getInputStream(), Charsets.UTF_8)); + } + + private MessageContent retrieveHtmlAndPlainTextContent(Multipart multipart) throws IOException { + Optional<String> textBody = getFirstMatchingTextBody(multipart, TEXT_PLAIN); + Optional<String> htmlBody = getFirstMatchingTextBody(multipart, TEXT_HTML); + MessageContent directChildTextBodies = new MessageContent(textBody, htmlBody); + if (!directChildTextBodies.isComplete()) { + MessageContent fromInnerMultipart = parseFirstFoundMultipart(multipart); + return directChildTextBodies.merge(fromInnerMultipart); + } + return directChildTextBodies; + } + + private MessageContent retrieveFirstReadablePart(Multipart multipart) throws IOException { + return retrieveFirstReadablePartMatching(multipart, this::isNotAttachment) + .orElseGet(() -> retrieveFirstReadablePartMatching(multipart, this::isInlinedWithoutCid) + .orElse(MessageContent.empty())); + } + + private Optional<MessageContent> retrieveFirstReadablePartMatching(Multipart multipart, Predicate<Entity> predicate) { + return multipart.getBodyParts() + .stream() + .filter(predicate) + .flatMap(Throwing.function(this::extractContentIfReadable).sneakyThrow()) + .findFirst(); + } + + private Stream<MessageContent> extractContentIfReadable(Entity entity) throws IOException { + if (TEXT_HTML.equals(entity.getMimeType()) && entity.getBody() instanceof TextBody) { + return Stream.of( + MessageContent.ofHtmlOnly(asString((TextBody)entity.getBody()))); + } + if (TEXT_PLAIN.equals(entity.getMimeType()) && entity.getBody() instanceof TextBody) { + return Stream.of( + MessageContent.ofTextOnly(asString((TextBody)entity.getBody()))); + } + if (entity.isMultipart() && entity.getBody() instanceof Multipart) { + MessageContent innerMultipartContent = parseMultipart(entity, (Multipart)entity.getBody()); + if (!innerMultipartContent.isEmpty()) { + return Stream.of(innerMultipartContent); + } + } + return Stream.empty(); + } + + private Optional<String> getFirstMatchingTextBody(Multipart multipart, String mimeType) throws IOException { + Optional<String> firstMatchingTextBody = getFirstMatchingTextBody(multipart, mimeType, this::isNotAttachment); + if (firstMatchingTextBody.isPresent()) { + return firstMatchingTextBody; + } + Optional<String> fallBackInlinedBodyWithoutCid = getFirstMatchingTextBody(multipart, mimeType, this::isInlinedWithoutCid); + return fallBackInlinedBodyWithoutCid; + } + + private Optional<String> getFirstMatchingTextBody(Multipart multipart, String mimeType, Predicate<Entity> condition) { + Function<TextBody, Optional<String>> textBodyOptionalFunction = Throwing + .<TextBody, Optional<String>>function(textBody -> asString(textBody)).sneakyThrow(); + + return multipart.getBodyParts() + .stream() + .filter(part -> mimeType.equals(part.getMimeType())) + .filter(condition) + .map(Entity::getBody) + .filter(TextBody.class::isInstance) + .map(TextBody.class::cast) + .findFirst() + .flatMap(textBodyOptionalFunction); + } + + private boolean isNotAttachment(Entity part) { + return part.getDispositionType() == null; + } + + private boolean isInlinedWithoutCid(Entity part) { + return part.getDispositionType().equals(MimeMessage.INLINE) && part.getHeader().getField(CONTENT_ID) == null; + } + + 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(Optional<String> textBody) { + return new MessageContent(textBody, Optional.empty()); + } + + public static MessageContent ofHtmlOnly(Optional<String> htmlBody) { + return new MessageContent(Optional.empty(), htmlBody); + } + + public static MessageContent empty() { + return new MessageContent(Optional.empty(), Optional.empty()); + } + + public Optional<String> getTextBody() { + return textBody; + } + + public Optional<String> getHtmlBody() { + return htmlBody; + } + + public boolean isEmpty() { + return equals(empty()); + } + + public boolean isComplete() { + return textBody.isPresent() && htmlBody.isPresent(); + } + + public MessageContent merge(MessageContent fromInnerMultipart) { + return new MessageContent( + textBody.map(Optional::of).orElse(fromInnerMultipart.getTextBody()), + htmlBody.map(Optional::of).orElse(fromInnerMultipart.getHtmlBody())); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof MessageContent)) { + return false; + } + MessageContent otherMessageContent = (MessageContent)other; + return Objects.equals(this.textBody, otherMessageContent.textBody) + && Objects.equals(this.htmlBody, otherMessageContent.htmlBody); + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/server/container/util-java8/src/test/java/org/apache/james/util/mime/MessageContentExtractorTest.java ---------------------------------------------------------------------- diff --git a/server/container/util-java8/src/test/java/org/apache/james/util/mime/MessageContentExtractorTest.java b/server/container/util-java8/src/test/java/org/apache/james/util/mime/MessageContentExtractorTest.java new file mode 100644 index 0000000..9607564 --- /dev/null +++ b/server/container/util-java8/src/test/java/org/apache/james/util/mime/MessageContentExtractorTest.java @@ -0,0 +1,491 @@ +/**************************************************************** + * 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.util.mime; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.Optional; + +import javax.mail.internet.MimeMessage; + +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.Multipart; +import org.apache.james.mime4j.field.Fields; +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.HeaderImpl; +import org.apache.james.mime4j.message.MessageBuilder; +import org.apache.james.mime4j.message.MultipartBuilder; +import org.apache.james.mime4j.stream.Field; +import org.apache.james.mime4j.util.ByteSequence; +import org.apache.james.util.mime.MessageContentExtractor; +import org.apache.james.util.mime.MessageContentExtractor.MessageContent; +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 static final String TEXT_CONTENT2 = "other text content"; + private static final String HTML_CONTENT2 = "other <b>html</b> content"; + private static final String ATTACHMENT_CONTENT = "attachment content"; + private static final String ANY_VALUE = "anyValue"; + private static final Field CONTENT_ID_FIELD = new Field() { + @Override + public String getName() { + return MessageContentExtractor.CONTENT_ID; + } + + @Override + public String getBody() { + return ANY_VALUE; + } + + @Override + public ByteSequence getRaw() { + return ByteSequence.EMPTY; + } + }; + + private MessageContentExtractor testee; + + private BodyPart htmlPart; + private BodyPart textPart; + private BodyPart textAttachment; + private BodyPart inlineText; + private BodyPart inlineImage; + + @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(); + textAttachment = BodyPartBuilder.create() + .setBody(ATTACHMENT_CONTENT, "plain", Charsets.UTF_8) + .setContentDisposition("attachment") + .build(); + inlineText = BodyPartBuilder.create() + .setBody(ATTACHMENT_CONTENT, "plain", Charsets.UTF_8) + .setContentDisposition("inline") + .build(); + inlineImage = BodyPartBuilder.create() + .setBody(new byte[0], "image/png") + .setContentDisposition("inline") + .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 extractShouldReturnFirstNonAttachmentPartWhenMultipartMixed() throws IOException { + Multipart multipart = MultipartBuilder.create("mixed") + .addBodyPart(textAttachment) + .addBodyPart(htmlPart) + .addBodyPart(textPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + assertThat(actual.getTextBody()).isEmpty(); + } + + @Test + public void extractShouldReturnInlinedTextBodyWithoutCIDWhenNoOtherValidParts() throws IOException { + String textBody = "body 1"; + Multipart multipart = MultipartBuilder.create("report") + .addBodyPart(BodyPartBuilder.create() + .setBody(textBody, "plain", Charsets.UTF_8) + .setContentDisposition("inline") + .build()) + .addBodyPart(BodyPartBuilder.create() + .setBody("body 2", "rfc822-headers", Charsets.UTF_8) + .setContentDisposition("inline") + .build()) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + + MessageContent actual = testee.extract(message); + + assertThat(actual.getTextBody()).contains(textBody); + } + + @Test + public void extractShouldReturnEmptyWhenMultipartMixedAndFirstPartIsATextAttachment() throws IOException { + Multipart multipart = MultipartBuilder.create("mixed") + .addBodyPart(textAttachment) + .build(); + Message message = MessageBuilder.create() + .setBody(multipart) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).isEmpty(); + 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); + } + + @Test + public void extractShouldReturnHtmlWhenMultipartRelated() throws IOException { + Multipart multipart = MultipartBuilder.create("related") + .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 extractShouldReturnHtmlAndTextWhenMultipartAlternativeAndFirstPartIsMultipartRelated() throws IOException { + BodyPart multipartRelated = BodyPartBuilder.create() + .setBody(MultipartBuilder.create("related") + .addBodyPart(htmlPart) + .build()) + .build(); + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(multipartRelated) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + MessageContent actual = testee.extract(message); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldRetrieveHtmlBodyWithOneInlinedHTMLAttachmentWithoutCid() throws IOException { + //Given + BodyPart inlinedHTMLPart = BodyPartBuilder.create() + .setBody(HTML_CONTENT, "html", Charsets.UTF_8) + .build(); + HeaderImpl inlinedHeader = new HeaderImpl(); + inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); + inlinedHeader.addField(Fields.contentType("text/html; charset=utf-8")); + inlinedHTMLPart.setHeader(inlinedHeader); + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(inlinedHTMLPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + + //When + MessageContent actual = testee.extract(message); + + //Then + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void extractShouldNotRetrieveHtmlBodyWithOneInlinedHTMLAttachmentWithCid() throws IOException { + //Given + BodyPart inlinedHTMLPart = BodyPartBuilder.create() + .setBody(HTML_CONTENT, "html", Charsets.UTF_8) + .build(); + HeaderImpl inlinedHeader = new HeaderImpl(); + inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); + inlinedHeader.addField(Fields.contentType("text/html; charset=utf-8")); + inlinedHeader.addField(CONTENT_ID_FIELD); + inlinedHTMLPart.setHeader(inlinedHeader); + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(inlinedHTMLPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + + //When + MessageContent actual = testee.extract(message); + + //Then + assertThat(actual.getHtmlBody()).isEmpty(); + } + + + @Test + public void extractShouldRetrieveTextBodyWithOneInlinedTextAttachmentWithoutCid() throws IOException { + //Given + BodyPart inlinedTextPart = BodyPartBuilder.create() + .setBody(TEXT_CONTENT, "text", Charsets.UTF_8) + .build(); + HeaderImpl inlinedHeader = new HeaderImpl(); + inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); + inlinedHeader.addField(Fields.contentType("text/plain; charset=utf-8")); + inlinedTextPart.setHeader(inlinedHeader); + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(inlinedTextPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + + //When + MessageContent actual = testee.extract(message); + + //Then + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + } + + @Test + public void extractShouldNotRetrieveTextBodyWithOneInlinedTextAttachmentWithCid() throws IOException { + //Given + BodyPart inlinedTextPart = BodyPartBuilder.create() + .setBody(TEXT_CONTENT, "text", Charsets.UTF_8) + .build(); + HeaderImpl inlinedHeader = new HeaderImpl(); + inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); + inlinedHeader.addField(Fields.contentType("text/plain; charset=utf-8")); + inlinedHeader.addField(CONTENT_ID_FIELD); + inlinedTextPart.setHeader(inlinedHeader); + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(inlinedTextPart) + .build(); + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + + //When + MessageContent actual = testee.extract(message); + + //Then + assertThat(actual.getTextBody()).isEmpty(); + } + + @Test + public void extractShouldRetrieveTextAndHtmlBodyWhenOneInlinedTextAttachmentAndMainContentInMultipart() throws IOException { + BodyPart multipartAlternative = BodyPartBuilder.create() + .setBody(MultipartBuilder.create("alternative") + .addBodyPart(textPart) + .addBodyPart(htmlPart) + .build()) + .build(); + + Multipart multipartMixed = MultipartBuilder.create("mixed") + .addBodyPart(multipartAlternative) + .addBodyPart(inlineText) + .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); + } + + @Test + public void extractShouldRetrieveTextBodyAndHtmlBodyWhenTextBodyInMainMultipartAndHtmlBodyInInnerMultipart() throws IOException { + BodyPart multipartRelated = BodyPartBuilder.create() + .setBody(MultipartBuilder.create("related") + .addBodyPart(htmlPart) + .addBodyPart(inlineImage) + .build()) + .build(); + + Multipart multipartAlternative = MultipartBuilder.create("alternative") + .addBodyPart(textPart) + .addBodyPart(multipartRelated) + .build(); + + Message message = MessageBuilder.create() + .setBody(multipartAlternative) + .build(); + + MessageContent actual = testee.extract(message); + assertThat(actual.getTextBody()).contains(TEXT_CONTENT); + assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); + } + + @Test + public void mergeMessageContentShouldReturnEmptyWhenAllEmpty() { + MessageContent messageContent1 = MessageContent.empty(); + MessageContent messageContent2 = MessageContent.empty(); + MessageContent expected = MessageContent.empty(); + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void mergeMessageContentShouldReturnFirstWhenSecondEmpty() { + MessageContent messageContent1 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); + MessageContent messageContent2 = MessageContent.empty(); + MessageContent expected = messageContent1; + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void mergeMessageContentShouldReturnSecondWhenFirstEmpty() { + MessageContent messageContent1 = MessageContent.empty(); + MessageContent messageContent2 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); + MessageContent expected = messageContent2; + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void mergeMessageContentShouldReturnMixWhenFirstTextOnlyAndSecondHtmlOnly() { + MessageContent messageContent1 = MessageContent.ofTextOnly(Optional.of(TEXT_CONTENT)); + MessageContent messageContent2 = MessageContent.ofHtmlOnly(Optional.of(HTML_CONTENT)); + MessageContent expected = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void mergeMessageContentShouldReturnMixWhenFirstHtmlOnlyAndSecondTextOnly() { + MessageContent messageContent1 = MessageContent.ofHtmlOnly(Optional.of(HTML_CONTENT)); + MessageContent messageContent2 = MessageContent.ofTextOnly(Optional.of(TEXT_CONTENT)); + MessageContent expected = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void mergeMessageContentShouldReturnFirstWhenTwiceAreComplete() { + MessageContent messageContent1 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); + MessageContent messageContent2 = new MessageContent(Optional.of(TEXT_CONTENT2), Optional.of(HTML_CONTENT2)); + MessageContent expected = messageContent1; + + MessageContent actual = messageContent1.merge(messageContent2); + + assertThat(actual).isEqualTo(expected); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 deleted file mode 100644 index 306f8c3..0000000 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageContentExtractor.java +++ /dev/null @@ -1,226 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ - -package org.apache.james.jmap.model; - -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -import javax.mail.internet.MimeMessage; - -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; -import com.github.fge.lambdas.functions.ThrowingFunction; -import com.google.common.base.Charsets; - -public class MessageContentExtractor { - - public static final String CONTENT_ID = "Content-ID"; - public static final String MULTIPART_ALTERNATIVE = "multipart/alternative"; - public static final String TEXT_HTML = "text/html"; - public static final String TEXT_PLAIN = "text/plain"; - - 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 { - Optional<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 { - MessageContent messageContent = parseMultipartContent(entity, multipart); - if (!messageContent.isEmpty()) { - return messageContent; - } - return parseFirstFoundMultipart(multipart); - } - - private MessageContent parseMultipartContent(Entity entity, Multipart multipart) throws IOException { - switch(entity.getMimeType()) { - case MULTIPART_ALTERNATIVE: - return retrieveHtmlAndPlainTextContent(multipart); - default: - return retrieveFirstReadablePart(multipart); - } - } - - private MessageContent parseFirstFoundMultipart(Multipart multipart) throws IOException { - ThrowingFunction<Entity, MessageContent> parseMultipart = firstPart -> parseMultipart(firstPart, (Multipart)firstPart.getBody()); - return multipart.getBodyParts() - .stream() - .filter(part -> part.getBody() instanceof Multipart) - .findFirst() - .map(Throwing.function(parseMultipart).sneakyThrow()) - .orElse(MessageContent.empty()); - } - - private Optional<String> asString(TextBody textBody) throws IOException { - return Optional.ofNullable(IOUtils.toString(textBody.getInputStream(), Charsets.UTF_8)); - } - - private MessageContent retrieveHtmlAndPlainTextContent(Multipart multipart) throws IOException { - Optional<String> textBody = getFirstMatchingTextBody(multipart, TEXT_PLAIN); - Optional<String> htmlBody = getFirstMatchingTextBody(multipart, TEXT_HTML); - MessageContent directChildTextBodies = new MessageContent(textBody, htmlBody); - if (!directChildTextBodies.isComplete()) { - MessageContent fromInnerMultipart = parseFirstFoundMultipart(multipart); - return directChildTextBodies.merge(fromInnerMultipart); - } - return directChildTextBodies; - } - - private MessageContent retrieveFirstReadablePart(Multipart multipart) throws IOException { - return retrieveFirstReadablePartMatching(multipart, this::isNotAttachment) - .orElseGet(() -> retrieveFirstReadablePartMatching(multipart, this::isInlinedWithoutCid) - .orElse(MessageContent.empty())); - } - - private Optional<MessageContent> retrieveFirstReadablePartMatching(Multipart multipart, Predicate<Entity> predicate) { - return multipart.getBodyParts() - .stream() - .filter(predicate) - .flatMap(Throwing.function(this::extractContentIfReadable).sneakyThrow()) - .findFirst(); - } - - private Stream<MessageContent> extractContentIfReadable(Entity entity) throws IOException { - if (TEXT_HTML.equals(entity.getMimeType()) && entity.getBody() instanceof TextBody) { - return Stream.of( - MessageContent.ofHtmlOnly(asString((TextBody)entity.getBody()))); - } - if (TEXT_PLAIN.equals(entity.getMimeType()) && entity.getBody() instanceof TextBody) { - return Stream.of( - MessageContent.ofTextOnly(asString((TextBody)entity.getBody()))); - } - if (entity.isMultipart() && entity.getBody() instanceof Multipart) { - MessageContent innerMultipartContent = parseMultipart(entity, (Multipart)entity.getBody()); - if (!innerMultipartContent.isEmpty()) { - return Stream.of(innerMultipartContent); - } - } - return Stream.empty(); - } - - private Optional<String> getFirstMatchingTextBody(Multipart multipart, String mimeType) throws IOException { - Optional<String> firstMatchingTextBody = getFirstMatchingTextBody(multipart, mimeType, this::isNotAttachment); - if (firstMatchingTextBody.isPresent()) { - return firstMatchingTextBody; - } - Optional<String> fallBackInlinedBodyWithoutCid = getFirstMatchingTextBody(multipart, mimeType, this::isInlinedWithoutCid); - return fallBackInlinedBodyWithoutCid; - } - - private Optional<String> getFirstMatchingTextBody(Multipart multipart, String mimeType, Predicate<Entity> condition) { - Function<TextBody, Optional<String>> textBodyOptionalFunction = Throwing - .<TextBody, Optional<String>>function(textBody -> asString(textBody)).sneakyThrow(); - - return multipart.getBodyParts() - .stream() - .filter(part -> mimeType.equals(part.getMimeType())) - .filter(condition) - .map(Entity::getBody) - .filter(TextBody.class::isInstance) - .map(TextBody.class::cast) - .findFirst() - .flatMap(textBodyOptionalFunction); - } - - private boolean isNotAttachment(Entity part) { - return part.getDispositionType() == null; - } - - private boolean isInlinedWithoutCid(Entity part) { - return part.getDispositionType().equals(MimeMessage.INLINE) && part.getHeader().getField(CONTENT_ID) == null; - } - - 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(Optional<String> textBody) { - return new MessageContent(textBody, Optional.empty()); - } - - public static MessageContent ofHtmlOnly(Optional<String> htmlBody) { - return new MessageContent(Optional.empty(), htmlBody); - } - - public static MessageContent empty() { - return new MessageContent(Optional.empty(), Optional.empty()); - } - - public Optional<String> getTextBody() { - return textBody; - } - - public Optional<String> getHtmlBody() { - return htmlBody; - } - - public boolean isEmpty() { - return equals(empty()); - } - - public boolean isComplete() { - return textBody.isPresent() && htmlBody.isPresent(); - } - - public MessageContent merge(MessageContent fromInnerMultipart) { - return new MessageContent( - textBody.map(Optional::of).orElse(fromInnerMultipart.getTextBody()), - htmlBody.map(Optional::of).orElse(fromInnerMultipart.getHtmlBody())); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof MessageContent)) { - return false; - } - MessageContent otherMessageContent = (MessageContent)other; - return Objects.equals(this.textBody, otherMessageContent.textBody) - && Objects.equals(this.htmlBody, otherMessageContent.htmlBody); - } - } -} http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 53c9d7f..e7b1f2c 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 @@ -37,7 +37,6 @@ import javax.inject.Inject; import javax.mail.Flags; import javax.mail.internet.SharedInputStream; -import org.apache.james.jmap.model.MessageContentExtractor.MessageContent; import org.apache.james.jmap.utils.HtmlTextExtractor; import org.apache.james.mailbox.MessageUid; import org.apache.james.mailbox.exception.MailboxException; @@ -52,6 +51,8 @@ import org.apache.james.mime4j.dom.address.MailboxList; import org.apache.james.mime4j.message.MessageBuilder; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; +import org.apache.james.util.mime.MessageContentExtractor; +import org.apache.james.util.mime.MessageContentExtractor.MessageContent; import com.github.steveash.guavate.Guavate; import com.google.common.base.Preconditions; http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 15873ee..db400db 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,7 +37,6 @@ 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.MessagePreviewGenerator; import org.apache.james.jmap.model.MessageProperties.MessageProperty; @@ -57,6 +56,7 @@ import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MailboxPath; import org.apache.james.mailbox.tika.extractor.TikaTextExtractor; import org.apache.james.metrics.logger.DefaultMetricFactory; +import org.apache.james.util.mime.MessageContentExtractor; import org.assertj.core.api.Condition; import org.assertj.core.data.MapEntry; import org.assertj.core.groups.Tuple; http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 6a0dd63..7a36140 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 @@ -42,7 +42,6 @@ import org.apache.james.jmap.model.BlobId; 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.MessageContentExtractor; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessagePreviewGenerator; import org.apache.james.jmap.model.MessageProperties.MessageProperty; @@ -68,6 +67,7 @@ import org.apache.james.mailbox.model.ComposedMessageId; import org.apache.james.mailbox.model.MailboxPath; import org.apache.james.mailbox.model.TestMessageId; import org.apache.james.metrics.api.NoopMetricFactory; +import org.apache.james.util.mime.MessageContentExtractor; import org.apache.mailet.Mail; import org.junit.Before; import org.junit.Ignore; http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 deleted file mode 100644 index f40b94f..0000000 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageContentExtractorTest.java +++ /dev/null @@ -1,490 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ -package org.apache.james.jmap.model; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; -import java.util.Optional; - -import javax.mail.internet.MimeMessage; - -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.field.Fields; -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.HeaderImpl; -import org.apache.james.mime4j.message.MessageBuilder; -import org.apache.james.mime4j.message.MultipartBuilder; -import org.apache.james.mime4j.stream.Field; -import org.apache.james.mime4j.util.ByteSequence; -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 static final String TEXT_CONTENT2 = "other text content"; - private static final String HTML_CONTENT2 = "other <b>html</b> content"; - private static final String ATTACHMENT_CONTENT = "attachment content"; - private static final String ANY_VALUE = "anyValue"; - private static final Field CONTENT_ID_FIELD = new Field() { - @Override - public String getName() { - return MessageContentExtractor.CONTENT_ID; - } - - @Override - public String getBody() { - return ANY_VALUE; - } - - @Override - public ByteSequence getRaw() { - return ByteSequence.EMPTY; - } - }; - - private MessageContentExtractor testee; - - private BodyPart htmlPart; - private BodyPart textPart; - private BodyPart textAttachment; - private BodyPart inlineText; - private BodyPart inlineImage; - - @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(); - textAttachment = BodyPartBuilder.create() - .setBody(ATTACHMENT_CONTENT, "plain", Charsets.UTF_8) - .setContentDisposition("attachment") - .build(); - inlineText = BodyPartBuilder.create() - .setBody(ATTACHMENT_CONTENT, "plain", Charsets.UTF_8) - .setContentDisposition("inline") - .build(); - inlineImage = BodyPartBuilder.create() - .setBody(new byte[0], "image/png") - .setContentDisposition("inline") - .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 extractShouldReturnFirstNonAttachmentPartWhenMultipartMixed() throws IOException { - Multipart multipart = MultipartBuilder.create("mixed") - .addBodyPart(textAttachment) - .addBodyPart(htmlPart) - .addBodyPart(textPart) - .build(); - Message message = MessageBuilder.create() - .setBody(multipart) - .build(); - MessageContent actual = testee.extract(message); - assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); - assertThat(actual.getTextBody()).isEmpty(); - } - - @Test - public void extractShouldReturnInlinedTextBodyWithoutCIDWhenNoOtherValidParts() throws IOException { - String textBody = "body 1"; - Multipart multipart = MultipartBuilder.create("report") - .addBodyPart(BodyPartBuilder.create() - .setBody(textBody, "plain", Charsets.UTF_8) - .setContentDisposition("inline") - .build()) - .addBodyPart(BodyPartBuilder.create() - .setBody("body 2", "rfc822-headers", Charsets.UTF_8) - .setContentDisposition("inline") - .build()) - .build(); - Message message = MessageBuilder.create() - .setBody(multipart) - .build(); - - MessageContent actual = testee.extract(message); - - assertThat(actual.getTextBody()).contains(textBody); - } - - @Test - public void extractShouldReturnEmptyWhenMultipartMixedAndFirstPartIsATextAttachment() throws IOException { - Multipart multipart = MultipartBuilder.create("mixed") - .addBodyPart(textAttachment) - .build(); - Message message = MessageBuilder.create() - .setBody(multipart) - .build(); - MessageContent actual = testee.extract(message); - assertThat(actual.getTextBody()).isEmpty(); - 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); - } - - @Test - public void extractShouldReturnHtmlWhenMultipartRelated() throws IOException { - Multipart multipart = MultipartBuilder.create("related") - .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 extractShouldReturnHtmlAndTextWhenMultipartAlternativeAndFirstPartIsMultipartRelated() throws IOException { - BodyPart multipartRelated = BodyPartBuilder.create() - .setBody(MultipartBuilder.create("related") - .addBodyPart(htmlPart) - .build()) - .build(); - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(multipartRelated) - .build(); - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - MessageContent actual = testee.extract(message); - assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); - } - - @Test - public void extractShouldRetrieveHtmlBodyWithOneInlinedHTMLAttachmentWithoutCid() throws IOException { - //Given - BodyPart inlinedHTMLPart = BodyPartBuilder.create() - .setBody(HTML_CONTENT, "html", Charsets.UTF_8) - .build(); - HeaderImpl inlinedHeader = new HeaderImpl(); - inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); - inlinedHeader.addField(Fields.contentType("text/html; charset=utf-8")); - inlinedHTMLPart.setHeader(inlinedHeader); - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(inlinedHTMLPart) - .build(); - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - - //When - MessageContent actual = testee.extract(message); - - //Then - assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); - } - - @Test - public void extractShouldNotRetrieveHtmlBodyWithOneInlinedHTMLAttachmentWithCid() throws IOException { - //Given - BodyPart inlinedHTMLPart = BodyPartBuilder.create() - .setBody(HTML_CONTENT, "html", Charsets.UTF_8) - .build(); - HeaderImpl inlinedHeader = new HeaderImpl(); - inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); - inlinedHeader.addField(Fields.contentType("text/html; charset=utf-8")); - inlinedHeader.addField(CONTENT_ID_FIELD); - inlinedHTMLPart.setHeader(inlinedHeader); - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(inlinedHTMLPart) - .build(); - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - - //When - MessageContent actual = testee.extract(message); - - //Then - assertThat(actual.getHtmlBody()).isEmpty(); - } - - - @Test - public void extractShouldRetrieveTextBodyWithOneInlinedTextAttachmentWithoutCid() throws IOException { - //Given - BodyPart inlinedTextPart = BodyPartBuilder.create() - .setBody(TEXT_CONTENT, "text", Charsets.UTF_8) - .build(); - HeaderImpl inlinedHeader = new HeaderImpl(); - inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); - inlinedHeader.addField(Fields.contentType("text/plain; charset=utf-8")); - inlinedTextPart.setHeader(inlinedHeader); - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(inlinedTextPart) - .build(); - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - - //When - MessageContent actual = testee.extract(message); - - //Then - assertThat(actual.getTextBody()).contains(TEXT_CONTENT); - } - - @Test - public void extractShouldNotRetrieveTextBodyWithOneInlinedTextAttachmentWithCid() throws IOException { - //Given - BodyPart inlinedTextPart = BodyPartBuilder.create() - .setBody(TEXT_CONTENT, "text", Charsets.UTF_8) - .build(); - HeaderImpl inlinedHeader = new HeaderImpl(); - inlinedHeader.addField(Fields.contentDisposition(MimeMessage.INLINE)); - inlinedHeader.addField(Fields.contentType("text/plain; charset=utf-8")); - inlinedHeader.addField(CONTENT_ID_FIELD); - inlinedTextPart.setHeader(inlinedHeader); - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(inlinedTextPart) - .build(); - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - - //When - MessageContent actual = testee.extract(message); - - //Then - assertThat(actual.getTextBody()).isEmpty(); - } - - @Test - public void extractShouldRetrieveTextAndHtmlBodyWhenOneInlinedTextAttachmentAndMainContentInMultipart() throws IOException { - BodyPart multipartAlternative = BodyPartBuilder.create() - .setBody(MultipartBuilder.create("alternative") - .addBodyPart(textPart) - .addBodyPart(htmlPart) - .build()) - .build(); - - Multipart multipartMixed = MultipartBuilder.create("mixed") - .addBodyPart(multipartAlternative) - .addBodyPart(inlineText) - .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); - } - - @Test - public void extractShouldRetrieveTextBodyAndHtmlBodyWhenTextBodyInMainMultipartAndHtmlBodyInInnerMultipart() throws IOException { - BodyPart multipartRelated = BodyPartBuilder.create() - .setBody(MultipartBuilder.create("related") - .addBodyPart(htmlPart) - .addBodyPart(inlineImage) - .build()) - .build(); - - Multipart multipartAlternative = MultipartBuilder.create("alternative") - .addBodyPart(textPart) - .addBodyPart(multipartRelated) - .build(); - - Message message = MessageBuilder.create() - .setBody(multipartAlternative) - .build(); - - MessageContent actual = testee.extract(message); - assertThat(actual.getTextBody()).contains(TEXT_CONTENT); - assertThat(actual.getHtmlBody()).contains(HTML_CONTENT); - } - - @Test - public void mergeMessageContentShouldReturnEmptyWhenAllEmpty() { - MessageContent messageContent1 = MessageContent.empty(); - MessageContent messageContent2 = MessageContent.empty(); - MessageContent expected = MessageContent.empty(); - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void mergeMessageContentShouldReturnFirstWhenSecondEmpty() { - MessageContent messageContent1 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); - MessageContent messageContent2 = MessageContent.empty(); - MessageContent expected = messageContent1; - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void mergeMessageContentShouldReturnSecondWhenFirstEmpty() { - MessageContent messageContent1 = MessageContent.empty(); - MessageContent messageContent2 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); - MessageContent expected = messageContent2; - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void mergeMessageContentShouldReturnMixWhenFirstTextOnlyAndSecondHtmlOnly() { - MessageContent messageContent1 = MessageContent.ofTextOnly(Optional.of(TEXT_CONTENT)); - MessageContent messageContent2 = MessageContent.ofHtmlOnly(Optional.of(HTML_CONTENT)); - MessageContent expected = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void mergeMessageContentShouldReturnMixWhenFirstHtmlOnlyAndSecondTextOnly() { - MessageContent messageContent1 = MessageContent.ofHtmlOnly(Optional.of(HTML_CONTENT)); - MessageContent messageContent2 = MessageContent.ofTextOnly(Optional.of(TEXT_CONTENT)); - MessageContent expected = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void mergeMessageContentShouldReturnFirstWhenTwiceAreComplete() { - MessageContent messageContent1 = new MessageContent(Optional.of(TEXT_CONTENT), Optional.of(HTML_CONTENT)); - MessageContent messageContent2 = new MessageContent(Optional.of(TEXT_CONTENT2), Optional.of(HTML_CONTENT2)); - MessageContent expected = messageContent1; - - MessageContent actual = messageContent1.merge(messageContent2); - - assertThat(actual).isEqualTo(expected); - } -} http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java index 1e0130c..af0abd2 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java @@ -41,6 +41,7 @@ import org.apache.james.mailbox.model.Cid; import org.apache.james.mailbox.model.MessageAttachment; import org.apache.james.mailbox.model.TestMessageId; import org.apache.james.mailbox.tika.extractor.TikaTextExtractor; +import org.apache.james.util.mime.MessageContentExtractor; import org.junit.Before; import org.junit.Test; http://git-wip-us.apache.org/repos/asf/james-project/blob/e1c486e2/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 c45574c..12a7ebc 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 @@ -31,7 +31,6 @@ 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.MessageFactory.MetaDataWithContent; import org.apache.james.jmap.model.MessagePreviewGenerator; @@ -41,6 +40,7 @@ import org.apache.james.mailbox.MessageUid; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.inmemory.InMemoryId; import org.apache.james.mailbox.model.TestMessageId; +import org.apache.james.util.mime.MessageContentExtractor; import org.apache.mailet.Mail; import org.apache.mailet.MailAddress; import org.junit.Before; --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org