Repository: james-project Updated Branches: refs/heads/master bfe79736e -> 4dc1d61b5
JAMES-1717 Provide message composition and text extraction as separate service and rely on Mailbox Text extractor Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/4dc1d61b Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/4dc1d61b Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/4dc1d61b Branch: refs/heads/master Commit: 4dc1d61b5e51472541abbc6cf37183abe4e19cf7 Parents: 5f4f804 Author: Benoit Tellier <[email protected]> Authored: Thu Jun 2 18:25:58 2016 +0700 Committer: Benoit Tellier <[email protected]> Committed: Fri Jun 3 19:38:15 2016 +0700 ---------------------------------------------------------------------- .../java/org/apache/james/jmap/JMAPModule.java | 5 + .../james/jmap/MailetPreconditionTest.java | 4 +- .../modules/data/MemoryDataJmapModule.java | 5 + server/protocols/jmap/pom.xml | 10 +- .../james/jmap/mailet/VacationMailet.java | 8 +- .../apache/james/jmap/mailet/VacationReply.java | 57 +------- .../james/jmap/utils/HtmlTextExtractor.java | 26 ++++ .../utils/MailboxBasedHtmlTextExtractor.java | 33 +++++ .../jmap/utils/MimeMessageBodyGenerator.java | 90 ++++++++++++ .../james/jmap/mailet/VacationMailetTest.java | 5 +- .../james/jmap/mailet/VacationReplyTest.java | 91 ++++++------ .../MailboxBasedHtmlTextExtractorTest.java | 110 +++++++++++++++ .../utils/MimeMessageBodyGeneratorTest.java | 140 +++++++++++++++++++ .../jmap/src/test/resources/example.html | 21 +++ 14 files changed, 501 insertions(+), 104 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java index 0744b49..40ff85f 100644 --- a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java +++ b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java @@ -29,6 +29,8 @@ import org.apache.commons.io.FileUtils; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.jmap.mailet.VacationMailet; import org.apache.james.jmap.methods.RequestHandler; +import org.apache.james.jmap.utils.HtmlTextExtractor; +import org.apache.james.jmap.utils.MailboxBasedHtmlTextExtractor; import org.apache.james.lifecycle.api.Configurable; import org.apache.james.mailbox.MailboxManager; import org.apache.james.mailetcontainer.impl.MatcherMailetPair; @@ -57,6 +59,9 @@ public class JMAPModule extends AbstractModule { install(new MethodsModule()); bind(JMAPServer.class).in(Scopes.SINGLETON); bind(RequestHandler.class).in(Scopes.SINGLETON); + bind(MailboxBasedHtmlTextExtractor.class).in(Scopes.SINGLETON); + + bind(HtmlTextExtractor.class).to(MailboxBasedHtmlTextExtractor.class); Multibinder.newSetBinder(binder(), ConfigurationPerformer.class).addBinding().to(MoveCapabilityPrecondition.class); Multibinder<CamelMailetContainerModule.TransportProcessorCheck> transportProcessorChecks = Multibinder.newSetBinder(binder(), CamelMailetContainerModule.TransportProcessorCheck.class); http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/container/guice/guice-common/src/test/java/org/apache/james/jmap/MailetPreconditionTest.java ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/jmap/MailetPreconditionTest.java b/server/container/guice/guice-common/src/test/java/org/apache/james/jmap/MailetPreconditionTest.java index ecfa619..21245c3 100644 --- a/server/container/guice/guice-common/src/test/java/org/apache/james/jmap/MailetPreconditionTest.java +++ b/server/container/guice/guice-common/src/test/java/org/apache/james/jmap/MailetPreconditionTest.java @@ -53,7 +53,7 @@ public class MailetPreconditionTest { @Test(expected = ConfigurationException.class) public void vacationMailetCheckShouldThrowOnWrongMatcher() throws Exception { - List<MatcherMailetPair> pairs = Lists.newArrayList(new MatcherMailetPair(new All(), new VacationMailet(null, null, null, null))); + List<MatcherMailetPair> pairs = Lists.newArrayList(new MatcherMailetPair(new All(), new VacationMailet(null, null, null, null, null))); new JMAPModule.VacationMailetCheck().check(pairs); } @@ -65,7 +65,7 @@ public class MailetPreconditionTest { @Test public void vacationMailetCheckShouldNotThrowIfValidPairPresent() throws Exception { - List<MatcherMailetPair> pairs = Lists.newArrayList(new MatcherMailetPair(new RecipientIsLocal(), new VacationMailet(null, null, null, null))); + List<MatcherMailetPair> pairs = Lists.newArrayList(new MatcherMailetPair(new RecipientIsLocal(), new VacationMailet(null, null, null, null, null))); new JMAPModule.VacationMailetCheck().check(pairs); } http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/data/MemoryDataJmapModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/data/MemoryDataJmapModule.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/data/MemoryDataJmapModule.java index 737328e..02924b1 100644 --- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/data/MemoryDataJmapModule.java +++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/data/MemoryDataJmapModule.java @@ -25,6 +25,8 @@ import org.apache.james.jmap.api.vacation.VacationRepository; import org.apache.james.jmap.memory.access.MemoryAccessTokenRepository; import org.apache.james.jmap.memory.vacation.MemoryNotificationRegistry; import org.apache.james.jmap.memory.vacation.MemoryVacationRepository; +import org.apache.james.mailbox.store.extractor.TextExtractor; +import org.apache.james.mailbox.tika.extractor.TikaTextExtractor; import com.google.inject.AbstractModule; import com.google.inject.Scopes; @@ -41,5 +43,8 @@ public class MemoryDataJmapModule extends AbstractModule { bind(MemoryNotificationRegistry.class).in(Scopes.SINGLETON); bind(NotificationRegistry.class).to(MemoryNotificationRegistry.class); + + bind(TikaTextExtractor.class).in(Scopes.SINGLETON); + bind(TextExtractor.class).to(TikaTextExtractor.class); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/pom.xml ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml index d800fa6..8851da0 100644 --- a/server/protocols/jmap/pom.xml +++ b/server/protocols/jmap/pom.xml @@ -179,6 +179,11 @@ </dependency> <dependency> <groupId>org.apache.james</groupId> + <artifactId>apache-james-mailbox-tika</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.james</groupId> <artifactId>apache-mailet-base</artifactId> <scope>test</scope> <type>test-jar</type> @@ -274,11 +279,6 @@ <artifactId>bcpkix-jdk15on</artifactId> </dependency> <dependency> - <groupId>org.jsoup</groupId> - <artifactId>jsoup</artifactId> - <version>1.9.2</version> - </dependency> - <dependency> <groupId>org.apache.james</groupId> <artifactId>apache-james-mailbox-store</artifactId> <scope>test</scope> http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java index c5ae96a..8d0ef5c 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java @@ -30,6 +30,7 @@ import org.apache.james.jmap.api.vacation.NotificationRegistry; import org.apache.james.jmap.api.vacation.RecipientId; import org.apache.james.jmap.api.vacation.Vacation; import org.apache.james.jmap.api.vacation.VacationRepository; +import org.apache.james.jmap.utils.MimeMessageBodyGenerator; import org.apache.james.util.date.ZonedDateTimeProvider; import org.apache.mailet.Mail; import org.apache.mailet.MailAddress; @@ -46,14 +47,17 @@ public class VacationMailet extends GenericMailet { private final ZonedDateTimeProvider zonedDateTimeProvider; private final AutomaticallySentMailDetector automaticallySentMailDetector; private final NotificationRegistry notificationRegistry; + private final MimeMessageBodyGenerator mimeMessageBodyGenerator; @Inject public VacationMailet(VacationRepository vacationRepository, ZonedDateTimeProvider zonedDateTimeProvider, - AutomaticallySentMailDetector automaticallySentMailDetector, NotificationRegistry notificationRegistry) { + AutomaticallySentMailDetector automaticallySentMailDetector, NotificationRegistry notificationRegistry, + MimeMessageBodyGenerator mimeMessageBodyGenerator) { this.vacationRepository = vacationRepository; this.zonedDateTimeProvider = zonedDateTimeProvider; this.automaticallySentMailDetector = automaticallySentMailDetector; this.notificationRegistry = notificationRegistry; + this.mimeMessageBodyGenerator = mimeMessageBodyGenerator; } @Override @@ -95,7 +99,7 @@ public class VacationMailet extends GenericMailet { VacationReply vacationReply = VacationReply.builder(processedMail) .receivedMailRecipient(recipient) .vacation(vacation) - .build(); + .build(mimeMessageBodyGenerator); sendNotification(vacationReply); notificationRegistry.register(AccountId.fromString(recipient.toString()), RecipientId.fromMailAddress(processedMail.getSender()), http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationReply.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationReply.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationReply.java index a848759..9ad8e0f 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationReply.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/VacationReply.java @@ -19,23 +19,16 @@ package org.apache.james.jmap.mailet; -import java.io.IOException; import java.util.List; -import javax.activation.DataHandler; import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import javax.mail.util.ByteArrayDataSource; import org.apache.james.jmap.api.vacation.Vacation; -import org.apache.james.mime4j.dom.field.ContentTypeField; +import org.apache.james.jmap.utils.MimeMessageBodyGenerator; import org.apache.mailet.Mail; import org.apache.mailet.MailAddress; import org.apache.mailet.base.AutomaticallySentMailDetector; -import org.jsoup.Jsoup; import com.github.fge.lambdas.Throwing; import com.google.common.base.Preconditions; @@ -45,7 +38,6 @@ public class VacationReply { public static final String FROM_HEADER = "from"; public static final String TO_HEADER = "to"; - public static final String MIXED = "mixed"; public static Builder builder(Mail originalMail) { return new Builder(originalMail); @@ -74,60 +66,21 @@ public class VacationReply { return this; } - public VacationReply build() throws MessagingException { + public VacationReply build(MimeMessageBodyGenerator mimeMessageBodyGenerator) throws MessagingException { Preconditions.checkState(mailRecipient != null, "Original recipient address should not be null"); Preconditions.checkState(originalMail.getSender() != null, "Original sender address should not be null"); - return new VacationReply(mailRecipient, ImmutableList.of(originalMail.getSender()), generateMimeMessage()); + return new VacationReply(mailRecipient, ImmutableList.of(originalMail.getSender()), generateMimeMessage(mimeMessageBodyGenerator)); } - private MimeMessage generateMimeMessage() throws MessagingException { + private MimeMessage generateMimeMessage(MimeMessageBodyGenerator mimeMessageBodyGenerator) throws MessagingException { MimeMessage reply = (MimeMessage) originalMail.getMessage().reply(NOT_REPLY_TO_ALL); vacation.getSubject().ifPresent(Throwing.consumer(subjectString -> reply.setHeader("subject", subjectString))); reply.setHeader(FROM_HEADER, mailRecipient.toString()); reply.setHeader(TO_HEADER, originalMail.getSender().toString()); reply.setHeader(AutomaticallySentMailDetector.AUTO_SUBMITTED_HEADER, AutomaticallySentMailDetector.AUTO_REPLIED_VALUE); - return addBody(reply); - } - - @SuppressWarnings("OptionalGetWithoutIsPresent") - private MimeMessage addBody(MimeMessage reply) throws MessagingException { - if (vacation.getHtmlBody().isPresent()) { - reply.setContent(generateMultipart()); - } else { - reply.setText(vacation.getTextBody().get()); - } - return reply; - } - - @SuppressWarnings("OptionalGetWithoutIsPresent") - private Multipart generateMultipart() throws MessagingException { - try { - Multipart multipart = new MimeMultipart(MIXED); - addTextPart(multipart, vacation.getHtmlBody().get(), "text/html"); - addTextPart(multipart, retrievePlainTextMessage(), ContentTypeField.TYPE_TEXT_PLAIN); - return multipart; - } catch (IOException e) { - throw new MessagingException("Cannot read specified content", e); - } - } - - @SuppressWarnings("OptionalGetWithoutIsPresent") - private String retrievePlainTextMessage() { - return vacation.getTextBody() - .orElseGet(() -> Jsoup.parse(vacation.getHtmlBody().get()).text()); - } - - private Multipart addTextPart(Multipart multipart, String text, String contentType) throws MessagingException, IOException { - MimeBodyPart textReasonPart = new MimeBodyPart(); - textReasonPart.setDataHandler( - new DataHandler( - new ByteArrayDataSource( - text, - contentType + "; charset=UTF-8"))); - multipart.addBodyPart(textReasonPart); - return multipart; + return mimeMessageBodyGenerator.from(reply, vacation.getTextBody(), vacation.getHtmlBody()); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/HtmlTextExtractor.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/HtmlTextExtractor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/HtmlTextExtractor.java new file mode 100644 index 0000000..6f9fd72 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/HtmlTextExtractor.java @@ -0,0 +1,26 @@ +/**************************************************************** + * 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.utils; + +public interface HtmlTextExtractor { + + String toPlainText(String html); + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractor.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractor.java new file mode 100644 index 0000000..db9f0d4 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractor.java @@ -0,0 +1,33 @@ + + +package org.apache.james.jmap.utils; + +import java.io.ByteArrayInputStream; + +import javax.inject.Inject; + +import org.apache.james.mailbox.store.extractor.TextExtractor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MailboxBasedHtmlTextExtractor implements HtmlTextExtractor { + + private static final Logger LOGGER = LoggerFactory.getLogger(MailboxBasedHtmlTextExtractor.class); + + private final TextExtractor textExtractor; + + @Inject + public MailboxBasedHtmlTextExtractor(TextExtractor textExtractor) { + this.textExtractor = textExtractor; + } + + @Override + public String toPlainText(String html) { + try { + return textExtractor.extractContent(new ByteArrayInputStream(html.getBytes()), "text/html", "").getTextualContent(); + } catch (Exception e) { + LOGGER.warn("Error extracting text from HTML", e); + return html; + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MimeMessageBodyGenerator.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MimeMessageBodyGenerator.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MimeMessageBodyGenerator.java new file mode 100644 index 0000000..e0704ca --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/MimeMessageBodyGenerator.java @@ -0,0 +1,90 @@ +/**************************************************************** + O * 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.utils; + +import java.io.IOException; +import java.util.Optional; + +import javax.activation.DataHandler; +import javax.inject.Inject; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.util.ByteArrayDataSource; + +import org.apache.james.mime4j.dom.field.ContentTypeField; + +import com.github.fge.lambdas.Throwing; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +public class MimeMessageBodyGenerator { + public static final String MIXED = "mixed"; + + private final HtmlTextExtractor htmlTextExtractor; + + @Inject + @VisibleForTesting + MimeMessageBodyGenerator(HtmlTextExtractor htmlTextExtractor) { + this.htmlTextExtractor = htmlTextExtractor; + } + + public MimeMessage from(MimeMessage messageHoldingHeaders, Optional<String> plainText, Optional<String> htmlText) throws MessagingException { + Preconditions.checkNotNull(messageHoldingHeaders); + Preconditions.checkNotNull(plainText); + Preconditions.checkNotNull(htmlText); + Preconditions.checkState(plainText.isPresent() || htmlText.isPresent(), "MimeMessageBodyGenerator needs at least plainText or html text"); + if (htmlText.isPresent()) { + messageHoldingHeaders.setContent(generateMultipart(htmlText.get(), plainText)); + } else { + plainText.ifPresent(Throwing.consumer(messageHoldingHeaders::setText)); + } + return messageHoldingHeaders; + } + + private Multipart generateMultipart(String htmlText, Optional<String> plainText) throws MessagingException { + try { + Multipart multipart = new MimeMultipart(MIXED); + addTextPart(multipart, htmlText, "text/html"); + addTextPart(multipart, retrievePlainTextMessage(plainText, htmlText), ContentTypeField.TYPE_TEXT_PLAIN); + return multipart; + } catch (IOException e) { + throw new MessagingException("Cannot read specified content", e); + } + } + + private Multipart addTextPart(Multipart multipart, String text, String contentType) throws MessagingException, IOException { + MimeBodyPart textReasonPart = new MimeBodyPart(); + textReasonPart.setDataHandler( + new DataHandler( + new ByteArrayDataSource( + text, + contentType + "; charset=UTF-8"))); + multipart.addBodyPart(textReasonPart); + return multipart; + } + + private String retrievePlainTextMessage(Optional<String> plainText, String htmlText) { + return plainText.orElseGet(() -> htmlTextExtractor.toPlainText(htmlText)); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java index f3047e4..1ba6995 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java @@ -38,6 +38,7 @@ import org.apache.james.jmap.api.vacation.NotificationRegistry; import org.apache.james.jmap.api.vacation.RecipientId; import org.apache.james.jmap.api.vacation.Vacation; import org.apache.james.jmap.api.vacation.VacationRepository; +import org.apache.james.jmap.utils.MimeMessageBodyGenerator; import org.apache.james.util.date.ZonedDateTimeProvider; import org.apache.mailet.MailAddress; import org.apache.mailet.MailetContext; @@ -66,6 +67,7 @@ public class VacationMailetTest { private VacationMailet testee; private VacationRepository vacationRepository; private ZonedDateTimeProvider zonedDateTimeProvider; + private MimeMessageBodyGenerator mimeMessageBodyGenerator; private MailetContext mailetContext; private MailAddress originalSender; private MailAddress originalRecipient; @@ -85,11 +87,12 @@ public class VacationMailetTest { .sender(originalSender) .build(); + mimeMessageBodyGenerator = mock(MimeMessageBodyGenerator.class); vacationRepository = mock(VacationRepository.class); zonedDateTimeProvider = mock(ZonedDateTimeProvider.class); automaticallySentMailDetector = mock(AutomaticallySentMailDetector.class); notificationRegistry = mock(NotificationRegistry.class); - testee = new VacationMailet(vacationRepository, zonedDateTimeProvider, automaticallySentMailDetector, notificationRegistry); + testee = new VacationMailet(vacationRepository, zonedDateTimeProvider, automaticallySentMailDetector, notificationRegistry, mimeMessageBodyGenerator); mailetContext = mock(MailetContext.class); testee.init(new FakeMailetConfig("vacation", mailetContext)); } http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationReplyTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationReplyTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationReplyTest.java index 5bb2ddd..73d8e69 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationReplyTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/VacationReplyTest.java @@ -20,30 +20,41 @@ package org.apache.james.jmap.mailet; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.Optional; import java.util.Properties; +import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.MimeMessage; -import org.apache.commons.io.IOUtils; import org.apache.james.jmap.api.vacation.Vacation; +import org.apache.james.jmap.utils.MimeMessageBodyGenerator; import org.apache.mailet.MailAddress; import org.apache.mailet.base.test.FakeMail; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.junit.Before; import org.junit.Test; +import com.google.common.base.Throwables; + public class VacationReplyTest { public static final String REASON = "I am in vacation dudes ! (plain text)"; public static final String HTML_REASON = "<b>I am in vacation dudes !</b> (html text)"; - public static final String HTML_EXTRACTED_REASON = "I am in vacation dudes ! (html text)"; public static final String SUBJECT = "subject"; + private MimeMessageBodyGenerator mimeMessageBodyGenerator; private MailAddress originalSender; private MailAddress originalRecipient; private FakeMail mail; + private MimeMessage generatedBody; @Before public void setUp() throws Exception { @@ -51,8 +62,13 @@ public class VacationReplyTest { originalRecipient = new MailAddress("[email protected]"); mail = new FakeMail(); - mail.setMessage(new MimeMessage(Session.getInstance(new Properties()) ,ClassLoader.getSystemResourceAsStream("spamMail.eml"))); + + mail.setMessage(new MimeMessage(Session.getInstance(new Properties()), ClassLoader.getSystemResourceAsStream("spamMail.eml"))); mail.setSender(originalSender); + + mimeMessageBodyGenerator = mock(MimeMessageBodyGenerator.class); + generatedBody = new MimeMessage(Session.getInstance(new Properties())); + when(mimeMessageBodyGenerator.from(any(MimeMessage.class), any(), any())).thenReturn(generatedBody); } @Test @@ -65,56 +81,27 @@ public class VacationReplyTest { .htmlBody(HTML_REASON) .build()) .receivedMailRecipient(originalRecipient) - .build(); - - assertThat(vacationReply.getRecipients()).containsExactly(originalSender); - assertThat(vacationReply.getSender()).isEqualTo(originalRecipient); - assertThat(IOUtils.toString(vacationReply.getMimeMessage().getInputStream())).contains(REASON); - assertThat(IOUtils.toString(vacationReply.getMimeMessage().getInputStream())).contains(HTML_REASON); - } - - @Test - public void vacationReplyShouldExtractPlainTextContentWhenOnlyHtmlBody() throws Exception { - VacationReply vacationReply = VacationReply.builder(mail) - .vacation(Vacation.builder() - .enabled(true) - .htmlBody(HTML_REASON) - .build()) - .receivedMailRecipient(originalRecipient) - .build(); + .build(mimeMessageBodyGenerator); assertThat(vacationReply.getRecipients()).containsExactly(originalSender); assertThat(vacationReply.getSender()).isEqualTo(originalRecipient); - assertThat(IOUtils.toString(vacationReply.getMimeMessage().getInputStream())).contains(HTML_EXTRACTED_REASON); - assertThat(IOUtils.toString(vacationReply.getMimeMessage().getInputStream())).contains(HTML_REASON); + assertThat(vacationReply.getMimeMessage()).isEqualTo(generatedBody); } @Test - public void vacationReplyShouldNotBeMultipartWhenVacationHaveNoHTML() throws Exception { + public void vacationReplyShouldAddReSuffixToSubjectByDefault() throws Exception { VacationReply vacationReply = VacationReply.builder(mail) .vacation(Vacation.builder() .enabled(true) .textBody(REASON) .build()) .receivedMailRecipient(originalRecipient) - .build(); + .build(mimeMessageBodyGenerator); + verify(mimeMessageBodyGenerator).from(argThat(createSubjectMatcher("Re: Original subject")), any(), any()); assertThat(vacationReply.getRecipients()).containsExactly(originalSender); assertThat(vacationReply.getSender()).isEqualTo(originalRecipient); - assertThat(IOUtils.toString(vacationReply.getMimeMessage().getInputStream())).isEqualTo(REASON); - } - - @Test - public void vacationReplyShouldAddReSuffixToSubjectByDefault() throws Exception { - VacationReply vacationReply = VacationReply.builder(mail) - .vacation(Vacation.builder() - .enabled(true) - .textBody(REASON) - .build()) - .receivedMailRecipient(originalRecipient) - .build(); - - assertThat(vacationReply.getMimeMessage().getHeader("subject")).containsExactly("Re: Original subject"); + assertThat(vacationReply.getMimeMessage()).isEqualTo(generatedBody); } @Test @@ -126,9 +113,12 @@ public class VacationReplyTest { .subject(Optional.of(SUBJECT)) .build()) .receivedMailRecipient(originalRecipient) - .build(); + .build(mimeMessageBodyGenerator); - assertThat(vacationReply.getMimeMessage().getHeader("subject")).containsExactly(SUBJECT); + verify(mimeMessageBodyGenerator).from(argThat(createSubjectMatcher(SUBJECT)), any(), any()); + assertThat(vacationReply.getRecipients()).containsExactly(originalSender); + assertThat(vacationReply.getSender()).isEqualTo(originalRecipient); + assertThat(vacationReply.getMimeMessage()).isEqualTo(generatedBody); } @Test(expected = NullPointerException.class) @@ -139,8 +129,25 @@ public class VacationReplyTest { @Test(expected = NullPointerException.class) public void vacationReplyShouldThrowOnNullOriginalEMailAddress() throws Exception { VacationReply.builder(new FakeMail()) - .receivedMailRecipient(null) - .build(); + .receivedMailRecipient(null); } + private BaseMatcher<MimeMessage> createSubjectMatcher(final String expectedSubject) { + return new BaseMatcher<MimeMessage>() { + @Override + public boolean matches(Object o) { + MimeMessage mimeMessage = (MimeMessage) o; + try { + return mimeMessage.getSubject().equals(expectedSubject); + } catch (MessagingException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void describeTo(Description description) { + + } + }; + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractorTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractorTest.java new file mode 100644 index 0000000..6697ba8 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MailboxBasedHtmlTextExtractorTest.java @@ -0,0 +1,110 @@ +/**************************************************************** + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.commons.io.IOUtils; +import org.apache.james.mailbox.tika.extractor.TikaTextExtractor; +import org.junit.Before; +import org.junit.Test; + +public class MailboxBasedHtmlTextExtractorTest { + + private MailboxBasedHtmlTextExtractor textExtractor; + + @Before + public void setUp() { + textExtractor = new MailboxBasedHtmlTextExtractor(new TikaTextExtractor()); + } + + @Test + public void toPlainTextShouldNotModifyPlainText() { + String textWithoutHtml = "text without html"; + assertThat(textExtractor.toPlainText(textWithoutHtml)).isEqualTo(textWithoutHtml); + } + + @Test + public void toPlainTextShouldRemoveSimpleHtmlTag() { + String html = "This is an <b>HTML</b> text !"; + String expectedPlainText = "This is an HTML text !"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + + @Test + public void toPlainTextShouldReplaceSkipLine() { + String html = "<p>This is an<br/>HTML text !</p>"; + String expectedPlainText = "This is an\nHTML text !\n"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + + @Test + public void toPlainTextShouldSkipLinesBetweenParagraph() { + String html = "<p>para1</p><p>para2</p>"; + String expectedPlainText = "para1\npara2\n"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + + @Test + public void nonClosedHtmlShouldBeTranslated() { + String html = "This is an <b>HTML text !"; + String expectedPlainText = "This is an HTML text !"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + + @Test + public void brokenHtmlShouldBeTranslatedUntilTheBrokenBalise() { + String html = "This is an <b>HTML</b missing missing missing !"; + String expectedPlainText = "This is an HTML"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + + @Test + public void toPlainTextShouldWorkWithMoreComplexHTML() throws Exception { + String html = IOUtils.toString(ClassLoader.getSystemResource("example.html")); + String expectedPlainText = "\n" + + " Why a new Logo?\n" + + "\n" + + "\n" + + "\n" + + " We are happy with our current logo, but for the\n" + + " upcoming James Server 3.0 release, we would like to\n" + + " give our community the opportunity to create a new image for James.\n" + + "\n" + + "\n" + + "\n" + + " Don't be shy, take your inkscape and gimp, and send us on\n" + + " the James Server User mailing list\n" + + " your creations. We will publish them on this page.\n" + + "\n" + + "\n" + + "\n" + + " We need an horizontal logo (100p height) to be show displayed on the upper\n" + + " left corner of this page, an avatar (48x48p) to be used on a Twitter stream for example.\n" + + " The used fonts should be redistributable (or commonly available on Windows and Linux).\n" + + " The chosen logo should be delivered in SVG format.\n" + + " We also like the Apache feather.\n" + + "\n" + + "\n" + + "\n"; + assertThat(textExtractor.toPlainText(html)).isEqualTo(expectedPlainText); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MimeMessageBodyGeneratorTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MimeMessageBodyGeneratorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MimeMessageBodyGeneratorTest.java new file mode 100644 index 0000000..b525660 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/MimeMessageBodyGeneratorTest.java @@ -0,0 +1,140 @@ +/**************************************************************** + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeMessage; + +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; + +public class MimeMessageBodyGeneratorTest { + + private MimeMessageBodyGenerator mimeMessageBodyGenerator; + private HtmlTextExtractor htmlTextExtractor; + private MimeMessage original; + + @Before + public void setUp() { + original = new MimeMessage(Session.getDefaultInstance(new Properties())); + htmlTextExtractor = mock(HtmlTextExtractor.class); + mimeMessageBodyGenerator = new MimeMessageBodyGenerator(htmlTextExtractor); + } + + @Test + public void fromShouldNotWriteAMultipartWhenOnlyPlainText() throws Exception { + assertThat(IOUtils.toString( + mimeMessageBodyGenerator.from(original, + Optional.of("Plain text"), + Optional.empty()) + .getInputStream())) + .isEqualTo("Plain text"); + verifyZeroInteractions(htmlTextExtractor); + } + + @Test + public void fromShouldPreservePreviouslySetHeaders() throws Exception { + String subject = "Important, I should be kept"; + original.setHeader("Subject", subject); + + mimeMessageBodyGenerator.from(original, + Optional.of("Plain text"), + Optional.empty()) + .getInputStream(); + + assertThat(original.getSubject()).isEqualTo(subject); + verifyZeroInteractions(htmlTextExtractor); + } + + @Test + public void fromShouldProvideAPlainTextVersionWhenOnlyHtml() throws Exception { + String htmlText = "<p>HTML text</p>"; + String plainText = "Plain text"; + when(htmlTextExtractor.toPlainText(htmlText)).thenReturn(plainText); + + String rowContent = IOUtils.toString( + mimeMessageBodyGenerator.from(original, + Optional.empty(), + Optional.of(htmlText)) + .getInputStream()); + + assertThat(rowContent).containsSequence(htmlText); + assertThat(rowContent).containsSequence(plainText); + } + + @Test + public void fromShouldCombinePlainTextAndHtml() throws Exception { + String htmlText = "<p>HTML text</p>"; + String plainText = "Plain text"; + + String rowContent = IOUtils.toString( + mimeMessageBodyGenerator.from(original, + Optional.of(plainText), + Optional.of(htmlText)) + .getInputStream()); + + assertThat(rowContent).containsSequence(htmlText); + assertThat(rowContent).containsSequence(plainText); + verifyZeroInteractions(htmlTextExtractor); + } + + @Test + public void fromShouldThrowWhenNoPlainTextNorHtmlBody() throws Exception { + assertThatThrownBy(() -> mimeMessageBodyGenerator.from(original, + Optional.empty(), + Optional.empty())) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void fromShouldThrowOnNullPlainText() throws Exception { + assertThatThrownBy(() -> mimeMessageBodyGenerator.from(original, + null, + Optional.empty())) + .isInstanceOf(NullPointerException.class); + } + + @Test + public void fromShouldThrowOnNullHtml() throws Exception { + assertThatThrownBy(() -> mimeMessageBodyGenerator.from(original, + Optional.empty(), + null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + public void fromShouldThrowOnNullMimeMessageToDecorate() throws Exception { + assertThatThrownBy(() -> mimeMessageBodyGenerator.from(null, + Optional.empty(), + Optional.empty())) + .isInstanceOf(NullPointerException.class); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/4dc1d61b/server/protocols/jmap/src/test/resources/example.html ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/resources/example.html b/server/protocols/jmap/src/test/resources/example.html new file mode 100644 index 0000000..59d3395 --- /dev/null +++ b/server/protocols/jmap/src/test/resources/example.html @@ -0,0 +1,21 @@ +<div class="section"> + <h3>Why a new Logo?<a name="Why_a_new_Logo"></a></h3> + + + <p>We are happy with our current logo, but for the + upcoming James Server 3.0 release, we would like to + give our community the opportunity to create a new image for James.</p> + + + <p>Don't be shy, take your inkscape and gimp, and send us on + the <a href="mail.html">James Server User mailing list</a> + your creations. We will publish them on this page.</p> + + + <p>We need an horizontal logo (100p height) to be show displayed on the upper + left corner of this page, an avatar (48x48p) to be used on a Twitter stream for example. + The used fonts should be redistributable (or commonly available on Windows and Linux). + The chosen logo should be delivered in SVG format. + We also like the <a class="externalLink" href="http://www.apache.org/foundation/press/kit/">Apache feather</a>.</p> + +</div> \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
