JAMES-2342 Allow Message un-learning when moved out of Spam
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/a6b91230 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/a6b91230 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/a6b91230 Branch: refs/heads/master Commit: a6b91230d008d762031cd4644af444197a48f292 Parents: 8b13708 Author: benwa <[email protected]> Authored: Wed Mar 7 14:12:43 2018 +0700 Committer: Antoine Duprat <[email protected]> Committed: Thu Mar 8 11:02:26 2018 +0100 ---------------------------------------------------------------------- .../mailbox/spamassassin/SpamAssassin.java | 9 + .../spamassassin/SpamAssassinListener.java | 31 ++- .../spamassassin/SpamAssassinListenerTest.java | 64 ++++- .../james/util/scanner/SpamAssassinInvoker.java | 41 +++ .../util/scanner/SpamAssassinInvokerTest.java | 57 +++- .../integration/SpamAssassinContract.java | 259 +++++++++++++++++++ .../apache/james/utils/IMAPMessageReader.java | 4 + 7 files changed, 452 insertions(+), 13 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassin.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassin.java b/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassin.java index 7eecfe9..fb27e9a 100644 --- a/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassin.java +++ b/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassin.java @@ -45,4 +45,13 @@ public class SpamAssassin { .forEach(Throwing.consumer(message -> invoker.learnAsSpam(message, user))); } } + + public void learnHam(List<InputStream> messages, String user) { + if (spamAssassinConfiguration.isEnable()) { + Host host = spamAssassinConfiguration.getHost().get(); + SpamAssassinInvoker invoker = new SpamAssassinInvoker(host.getHostName(), host.getPort()); + messages + .forEach(Throwing.consumer(message -> invoker.learnAsHam(message, user))); + } + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassinListener.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassinListener.java b/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassinListener.java index 7a6382b..63a8edf 100644 --- a/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassinListener.java +++ b/mailbox/plugin/spamassassin/src/main/java/org/apache/james/mailbox/spamassassin/SpamAssassinListener.java @@ -68,16 +68,24 @@ public class SpamAssassinListener implements SpamEventListener { MessageMoveEvent messageMoveEvent = (MessageMoveEvent) event; if (isMessageMovedToSpamMailbox(messageMoveEvent)) { LOGGER.debug("Spam event detected"); - ImmutableList<InputStream> messages = messageMoveEvent.getMessages() - .values() - .stream() - .map(Throwing.function(Message::getFullContent)) - .collect(Guavate.toImmutableList()); + ImmutableList<InputStream> messages = retrieveMessages(messageMoveEvent); spamAssassin.learnSpam(messages, messageMoveEvent.getSession().getUser().getUserName()); } + if (isMessageMovedOutOfSpamMailbox(messageMoveEvent)) { + ImmutableList<InputStream> messages = retrieveMessages(messageMoveEvent); + spamAssassin.learnHam(messages, messageMoveEvent.getSession().getUser().getUserName()); + } } } + public ImmutableList<InputStream> retrieveMessages(MessageMoveEvent messageMoveEvent) { + return messageMoveEvent.getMessages() + .values() + .stream() + .map(Throwing.function(Message::getFullContent)) + .collect(Guavate.toImmutableList()); + } + @VisibleForTesting boolean isMessageMovedToSpamMailbox(MessageMoveEvent event) { try { @@ -90,4 +98,17 @@ public class SpamAssassinListener implements SpamEventListener { return false; } } + + @VisibleForTesting + boolean isMessageMovedOutOfSpamMailbox(MessageMoveEvent event) { + try { + MailboxPath spamMailboxPath = MailboxPath.forUser(event.getSession().getUser().getUserName(), Role.SPAM.getDefaultMailbox()); + MailboxId spamMailboxId = mapperFactory.getMailboxMapper(event.getSession()).findMailboxByPath(spamMailboxPath).getMailboxId(); + + return event.getMessageMoves().removedMailboxIds().contains(spamMailboxId); + } catch (MailboxException e) { + LOGGER.warn("Could not resolve Spam mailbox", e); + return false; + } + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/mailbox/plugin/spamassassin/src/test/java/org/apache/james/mailbox/spamassassin/SpamAssassinListenerTest.java ---------------------------------------------------------------------- diff --git a/mailbox/plugin/spamassassin/src/test/java/org/apache/james/mailbox/spamassassin/SpamAssassinListenerTest.java b/mailbox/plugin/spamassassin/src/test/java/org/apache/james/mailbox/spamassassin/SpamAssassinListenerTest.java index eab5267..ab0f78b 100644 --- a/mailbox/plugin/spamassassin/src/test/java/org/apache/james/mailbox/spamassassin/SpamAssassinListenerTest.java +++ b/mailbox/plugin/spamassassin/src/test/java/org/apache/james/mailbox/spamassassin/SpamAssassinListenerTest.java @@ -127,7 +127,7 @@ public class SpamAssassinListenerTest { } @Test - public void eventShouldCallSpamAssassinWhenTheEventMatches() { + public void eventShouldCallSpamAssassinSpamLearningWhenTheEventMatches() { MessageMoveEvent messageMoveEvent = MessageMoveEvent.builder() .session(MAILBOX_SESSION) .messageMoves(MessageMoves.builder() @@ -143,6 +143,68 @@ public class SpamAssassinListenerTest { verify(spamAssassin).learnSpam(any(), any()); } + @Test + public void isMessageMovedOutOfSpamMailboxShouldReturnFalseWhenMessageMovedBetweenNonSpamMailboxes() { + MessageMoveEvent messageMoveEvent = MessageMoveEvent.builder() + .session(MAILBOX_SESSION) + .messageMoves(MessageMoves.builder() + .previousMailboxIds(mailboxId1) + .targetMailboxIds(mailboxId2) + .build()) + .messages(ImmutableMap.of(MessageUid.of(45), + createMessage(mailboxId2))) + .build(); + + assertThat(listener.isMessageMovedOutOfSpamMailbox(messageMoveEvent)).isFalse(); + } + + @Test + public void isMessageMovedOutOfSpamMailboxShouldReturnFalseWhenMessageMovedOutOfCapitalSpamMailbox() { + MessageMoveEvent messageMoveEvent = MessageMoveEvent.builder() + .session(MAILBOX_SESSION) + .messageMoves(MessageMoves.builder() + .previousMailboxIds(spamCapitalMailboxId) + .targetMailboxIds(mailboxId2) + .build()) + .messages(ImmutableMap.of(MessageUid.of(45), + createMessage(mailboxId2))) + .build(); + + assertThat(listener.isMessageMovedOutOfSpamMailbox(messageMoveEvent)).isFalse(); + } + + @Test + public void isMessageMovedOutOfSpamMailboxShouldReturnTrueWhenMessageMovedOutOfSpamMailbox() { + MessageMoveEvent messageMoveEvent = MessageMoveEvent.builder() + .session(MAILBOX_SESSION) + .messageMoves(MessageMoves.builder() + .previousMailboxIds(spamMailboxId) + .targetMailboxIds(mailboxId2) + .build()) + .messages(ImmutableMap.of(MessageUid.of(45), + createMessage(mailboxId2))) + .build(); + + assertThat(listener.isMessageMovedOutOfSpamMailbox(messageMoveEvent)).isTrue(); + } + + @Test + public void eventShouldCallSpamAssassinHamLearningWhenTheEventMatches() { + MessageMoveEvent messageMoveEvent = MessageMoveEvent.builder() + .session(MAILBOX_SESSION) + .messageMoves(MessageMoves.builder() + .previousMailboxIds(spamMailboxId) + .targetMailboxIds(mailboxId1) + .build()) + .messages(ImmutableMap.of(MessageUid.of(45), + createMessage(mailboxId1))) + .build(); + + listener.event(messageMoveEvent); + + verify(spamAssassin).learnHam(any(), any()); + } + private SimpleMailboxMessage createMessage(MailboxId mailboxId) { int size = 45; int bodyStartOctet = 25; http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/server/container/util/src/main/java/org/apache/james/util/scanner/SpamAssassinInvoker.java ---------------------------------------------------------------------- diff --git a/server/container/util/src/main/java/org/apache/james/util/scanner/SpamAssassinInvoker.java b/server/container/util/src/main/java/org/apache/james/util/scanner/SpamAssassinInvoker.java index 0bcae3f..68805ab 100644 --- a/server/container/util/src/main/java/org/apache/james/util/scanner/SpamAssassinInvoker.java +++ b/server/container/util/src/main/java/org/apache/james/util/scanner/SpamAssassinInvoker.java @@ -193,6 +193,47 @@ public class SpamAssassinInvoker { } } + /** + * Tell spamd that the given MimeMessage is a ham. + * + * @param message + * The MimeMessage to tell + * @throws MessagingException + * if an error occured during learning. + */ + public boolean learnAsHam(InputStream message, String user) throws MessagingException { + try (Socket socket = new Socket(spamdHost, spamdPort); + OutputStream out = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(out); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { + + byte[] byteArray = IOUtils.toByteArray(message); + writer.write("TELL SPAMC/1.2"); + writer.write(CRLF); + writer.write("Content-length: " + byteArray.length); + writer.write(CRLF); + writer.write("Message-class: ham"); + writer.write(CRLF); + writer.write("Set: local, remote"); + writer.write(CRLF); + writer.write("User: " + user); + writer.write(CRLF); + writer.write(CRLF); + writer.flush(); + + out.write(byteArray); + out.flush(); + socket.shutdownOutput(); + + return in.lines() + .anyMatch(this::hasBeenSet); + } catch (UnknownHostException e) { + throw new MessagingException("Error communicating with spamd. Unknown host: " + spamdHost); + } catch (IOException e) { + throw new MessagingException("Error communicating with spamd on " + spamdHost + ":" + spamdPort + " Exception: " + e); + } + } + private boolean hasBeenSet(String line) { return line.startsWith("DidSet: local"); } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/server/container/util/src/test/java/org/apache/james/util/scanner/SpamAssassinInvokerTest.java ---------------------------------------------------------------------- diff --git a/server/container/util/src/test/java/org/apache/james/util/scanner/SpamAssassinInvokerTest.java b/server/container/util/src/test/java/org/apache/james/util/scanner/SpamAssassinInvokerTest.java index 6d6f9dc..0f995a0 100644 --- a/server/container/util/src/test/java/org/apache/james/util/scanner/SpamAssassinInvokerTest.java +++ b/server/container/util/src/test/java/org/apache/james/util/scanner/SpamAssassinInvokerTest.java @@ -20,6 +20,9 @@ package org.apache.james.util.scanner; import static org.assertj.core.api.Assertions.assertThat; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + import javax.mail.internet.MimeMessage; import org.apache.james.util.MimeMessageUtil; @@ -31,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SpamAssassinExtension.class) public class SpamAssassinInvokerTest { + public static final String USER = "any@james"; private SpamAssassin spamAssassin; private SpamAssassinInvoker testee; @@ -44,7 +48,7 @@ public class SpamAssassinInvokerTest { public void scanMailShouldModifyHitsField() throws Exception { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("eml/spam.eml")); - SpamAssassinResult result = testee.scanMail(mimeMessage, "any@james"); + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); assertThat(result.getHits()).isNotEqualTo(SpamAssassinResult.NO_RESULT); } @@ -53,7 +57,7 @@ public class SpamAssassinInvokerTest { public void scanMailShouldModifyRequiredHitsField() throws Exception { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("eml/spam.eml")); - SpamAssassinResult result = testee.scanMail(mimeMessage, "any@james"); + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); assertThat(result.getRequiredHits()).isEqualTo("5.0"); } @@ -62,7 +66,7 @@ public class SpamAssassinInvokerTest { public void scanMailShouldModifyHeadersField() throws Exception { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("eml/spam.eml")); - SpamAssassinResult result = testee.scanMail(mimeMessage, "any@james"); + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); assertThat(result.getHeadersAsAttribute()).isNotEmpty(); } @@ -74,7 +78,7 @@ public class SpamAssassinInvokerTest { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("spamassassin_db/spam/spam1")); - SpamAssassinResult result = testee.scanMail(mimeMessage, "any@james"); + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); assertThat(result.getHeadersAsAttribute().get(SpamAssassinResult.FLAG_MAIL_ATTRIBUTE_NAME)).isEqualTo("YES"); } @@ -84,7 +88,7 @@ public class SpamAssassinInvokerTest { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("spamassassin_db/spam/spam2")); - boolean result = testee.learnAsSpam(mimeMessage.getInputStream(), "any@james"); + boolean result = testee.learnAsSpam(mimeMessage.getInputStream(), USER); assertThat(result).isTrue(); } @@ -94,10 +98,49 @@ public class SpamAssassinInvokerTest { MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( ClassLoader.getSystemResourceAsStream("spamassassin_db/spam/spam1")); - testee.learnAsSpam(mimeMessage.getInputStream(), "any@james"); + byte[] messageAsBytes = MimeMessageUtil.asString(mimeMessage).getBytes(StandardCharsets.UTF_8); + + testee.learnAsSpam(new ByteArrayInputStream(messageAsBytes), USER); - SpamAssassinResult result = testee.scanMail(mimeMessage, "any@james"); + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); assertThat(result.getHeadersAsAttribute().get(SpamAssassinResult.FLAG_MAIL_ATTRIBUTE_NAME)).isEqualTo("YES"); } + + @Test + public void learnAsHamShouldReturnTrueWhenLearningWorks() throws Exception { + MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( + ClassLoader.getSystemResourceAsStream("spamassassin_db/ham/ham2")); + + boolean result = testee.learnAsHam(mimeMessage.getInputStream(), USER); + + assertThat(result).isTrue(); + } + + @Test + public void scanMailShouldMarkAsHamWhenMessageAlreadyLearnedAsHam() throws Exception { + MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( + ClassLoader.getSystemResourceAsStream("spamassassin_db/ham/ham1")); + + testee.learnAsHam(mimeMessage.getInputStream(), USER); + + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); + + assertThat(result.getHeadersAsAttribute().get(SpamAssassinResult.FLAG_MAIL_ATTRIBUTE_NAME)).isEqualTo("NO"); + } + + @Test + public void learnAsHamShouldAllowToForgetSpam() throws Exception { + MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream( + ClassLoader.getSystemResourceAsStream("eml/spam.eml")); + + byte[] messageAsBytes = MimeMessageUtil.asString(mimeMessage).getBytes(StandardCharsets.UTF_8); + + testee.learnAsSpam(new ByteArrayInputStream(messageAsBytes), USER); + testee.learnAsHam(new ByteArrayInputStream(messageAsBytes), USER); + + SpamAssassinResult result = testee.scanMail(mimeMessage, USER); + + assertThat(result.getHeadersAsAttribute().get(SpamAssassinResult.FLAG_MAIL_ATTRIBUTE_NAME)).isEqualTo("NO"); + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SpamAssassinContract.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SpamAssassinContract.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SpamAssassinContract.java index 6c75381..0968d40 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SpamAssassinContract.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SpamAssassinContract.java @@ -253,6 +253,265 @@ public interface SpamAssassinContract { calmlyAwait.atMost(10, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 2)); } + @Test + default void spamAssassinShouldForgetMessagesMovedOutOfSpamFolderUsingJMAP(JamesWithSpamAssassin james) throws Exception { + Duration slowPacedPollInterval = Duration.FIVE_HUNDRED_MILLISECONDS; + ConditionFactory calmlyAwait = Awaitility.with().pollInterval(slowPacedPollInterval).and().with().pollDelay(slowPacedPollInterval).await(); + + james.getSpamAssassinExtension().getSpamAssassin().train(ALICE); + AccessToken aliceAccessToken = accessTokenFor(james.getJmapServer(), ALICE, ALICE_PASSWORD); + AccessToken bobAccessToken = accessTokenFor(james.getJmapServer(), BOB, BOB_PASSWORD); + + // Bob is sending a message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Alice is moving this message to Spam -> learning in SpamAssassin + List<String> messageIds = with() + .header("Authorization", aliceAccessToken.serialize()) + .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + getInboxId(aliceAccessToken) + "\"]}}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messageList")) + .body(ARGUMENTS + ".messageIds", hasSize(1)) + .extract() + .path(ARGUMENTS + ".messageIds"); + + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"mailboxIds\": [\"" + getSpamId(aliceAccessToken) + "\"] } } }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".updated", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + + // Alice is moving this message out of Spam -> forgetting in SpamAssassin + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"mailboxIds\": [\"" + getInboxId(aliceAccessToken) + "\"] } } }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".updated", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Bob is sending again the same message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + + // This message is delivered in Alice INBOX mailbox (she now must have 2 messages in her Inbox mailbox) + calmlyAwait.atMost(10, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 2)); + } + + @Test + default void spamAssassinShouldForgetMessagesMovedOutOfSpamFolderUsingIMAP(JamesWithSpamAssassin james) throws Exception { + Duration slowPacedPollInterval = Duration.FIVE_HUNDRED_MILLISECONDS; + ConditionFactory calmlyAwait = Awaitility.with().pollInterval(slowPacedPollInterval).and().with().pollDelay(slowPacedPollInterval).await(); + + james.getSpamAssassinExtension().getSpamAssassin().train(ALICE); + AccessToken aliceAccessToken = accessTokenFor(james.getJmapServer(), ALICE, ALICE_PASSWORD); + AccessToken bobAccessToken = accessTokenFor(james.getJmapServer(), BOB, BOB_PASSWORD); + + // Bob is sending a message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Alice is moving this message to Spam -> learning in SpamAssassin + List<String> messageIds = with() + .header("Authorization", aliceAccessToken.serialize()) + .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + getInboxId(aliceAccessToken) + "\"]}}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messageList")) + .body(ARGUMENTS + ".messageIds", hasSize(1)) + .extract() + .path(ARGUMENTS + ".messageIds"); + + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"mailboxIds\": [\"" + getSpamId(aliceAccessToken) + "\"] } } }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".updated", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + + // Alice is moving this message out of Spam -> forgetting in SpamAssassin + try (IMAPMessageReader imapMessageReader = new IMAPMessageReader()) { + imapMessageReader.connect(LOCALHOST, IMAP_PORT) + .login(ALICE, ALICE_PASSWORD) + .select("Spam"); + + imapMessageReader.moveFirstMessage(IMAPMessageReader.INBOX); + } + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Bob is sending again the same message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + + // This message is delivered in Alice INBOX mailbox (she now must have 2 messages in her Inbox mailbox) + calmlyAwait.atMost(10, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 2)); + } + + @Test + default void expungingSpamMessageShouldNotImpactSpamAssassinState(JamesWithSpamAssassin james) throws Exception { + Duration slowPacedPollInterval = Duration.FIVE_HUNDRED_MILLISECONDS; + ConditionFactory calmlyAwait = Awaitility.with().pollInterval(slowPacedPollInterval).and().with().pollDelay(slowPacedPollInterval).await(); + + james.getSpamAssassinExtension().getSpamAssassin().train(ALICE); + AccessToken aliceAccessToken = accessTokenFor(james.getJmapServer(), ALICE, ALICE_PASSWORD); + AccessToken bobAccessToken = accessTokenFor(james.getJmapServer(), BOB, BOB_PASSWORD); + + // Bob is sending a message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Alice is moving this message to Spam -> learning in SpamAssassin + List<String> messageIds = with() + .header("Authorization", aliceAccessToken.serialize()) + .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + getInboxId(aliceAccessToken) + "\"]}}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messageList")) + .body(ARGUMENTS + ".messageIds", hasSize(1)) + .extract() + .path(ARGUMENTS + ".messageIds"); + + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"mailboxIds\": [\"" + getSpamId(aliceAccessToken) + "\"] } } }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".updated", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + + // Alice is deleting this message + try (IMAPMessageReader imapMessageReader = new IMAPMessageReader()) { + imapMessageReader.connect(LOCALHOST, IMAP_PORT) + .login(ALICE, ALICE_PASSWORD) + .select("Spam"); + + imapMessageReader.setFlagsForAllMessagesInMailbox("\\Deleted"); + imapMessageReader.expunge(); + } + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 0)); + + // Bob is sending again the same message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + + // This message is delivered in Alice SPAM mailbox (she now must have 1 messages in her Spam mailbox) + calmlyAwait.atMost(10, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + } + + @Test + default void deletingSpamMessageShouldNotImpactSpamAssassinState(JamesWithSpamAssassin james) throws Exception { + Duration slowPacedPollInterval = Duration.FIVE_HUNDRED_MILLISECONDS; + ConditionFactory calmlyAwait = Awaitility.with().pollInterval(slowPacedPollInterval).and().with().pollDelay(slowPacedPollInterval).await(); + + james.getSpamAssassinExtension().getSpamAssassin().train(ALICE); + AccessToken aliceAccessToken = accessTokenFor(james.getJmapServer(), ALICE, ALICE_PASSWORD); + AccessToken bobAccessToken = accessTokenFor(james.getJmapServer(), BOB, BOB_PASSWORD); + + // Bob is sending a message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getInboxId(aliceAccessToken), 1)); + + // Alice is moving this message to Spam -> learning in SpamAssassin + List<String> messageIds = with() + .header("Authorization", aliceAccessToken.serialize()) + .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + getInboxId(aliceAccessToken) + "\"]}}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messageList")) + .body(ARGUMENTS + ".messageIds", hasSize(1)) + .extract() + .path(ARGUMENTS + ".messageIds"); + + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"mailboxIds\": [\"" + getSpamId(aliceAccessToken) + "\"] } } }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".updated", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + + // Alice is deleting this message + messageIds + .forEach(messageId -> given() + .header("Authorization", aliceAccessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"destroy\": [\"%s\"] }, \"#0\"]]", messageId)) + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".destroyed", hasSize(1))); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 0)); + + // Bob is sending again the same message to Alice + given() + .header("Authorization", bobAccessToken.serialize()) + .body(setMessageCreate(bobAccessToken)) + .when() + .post("/jmap"); + + // This message is delivered in Alice SPAM mailbox (she now must have 1 messages in her Spam mailbox) + calmlyAwait.atMost(10, TimeUnit.SECONDS).until(() -> areMessagesFoundInMailbox(aliceAccessToken, getSpamId(aliceAccessToken), 1)); + } + default boolean areMessagesFoundInMailbox(AccessToken accessToken, String mailboxId, int expectedNumberOfMessages) { try { with() http://git-wip-us.apache.org/repos/asf/james-project/blob/a6b91230/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java ---------------------------------------------------------------------- diff --git a/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java b/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java index 2b7a7f0..258157c 100644 --- a/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java +++ b/server/testing/src/main/java/org/apache/james/utils/IMAPMessageReader.java @@ -172,4 +172,8 @@ public class IMAPMessageReader extends ExternalResource implements Closeable { public void moveFirstMessage(String destMailbox) throws IOException { imapClient.sendCommand("MOVE 1 " + destMailbox); } + + public void expunge() throws IOException { + imapClient.expunge(); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
