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]

Reply via email to