JAMES-3775 Mailet for RSpamD
diff --git a/server/apps/distributed-app/docs/modules/ROOT/partials/IsMarkedAsSpam.adoc b/server/apps/distributed-app/docs/modules/ROOT/partials/IsMarkedAsSpam.adoc
index 1f2bf1e..1c743ea 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/partials/IsMarkedAsSpam.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/partials/IsMarkedAsSpam.adoc
@@ -16,12 +16,13 @@
 <!-- End of SpamAssassing mailets pipeline -->
 ....
 
-In order to use this with `rspamd`, we need to declare a condition for the matcher.
+In order to use this with `rspamd`, we need to declare a condition for the matcher
+and drop the RSpamD jar (*third-party/rspamd*) in the James extensions-jars folder.
 Eg: With the recipient header for RSpamD being *org.apache.james.rspamd.status*,
 then the configuration would be:
 
 ....
-<!-- SpamAssassing mailets pipeline -->
+<!-- RSpamD mailets pipeline -->
     <mailet match="IsMarkedAsSpam=org.apache.james.rspamd.status" class="WithStorageDirective">
         <targetFolderName>Spam</targetFolderName>
     </mailet>
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/IsMarkedAsSpamTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/IsMarkedAsSpamTest.java
index 5841ee5..3ea229a 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/IsMarkedAsSpamTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/IsMarkedAsSpamTest.java
@@ -168,10 +168,10 @@
             .recipient("to@james.org")
             .addHeaderForRecipient(PerRecipientHeaders.Header.builder()
                     .name("custom.package")
-                    .value("Yes, hits=6.8 required=5.0")
+                    .value("Yes, actions=reject score=14.225 requiredScore=14.0 desiredRewriteSubject=")
                     .build(),
                 new MailAddress("to@james.org"))
-            .attribute(Attribute.convertToAttribute("custom.package", "Yes, hits=6.8 required=5.0"))
+            .attribute(Attribute.convertToAttribute("custom.package", "Yes, actions=reject score=14.225 requiredScore=14.0 desiredRewriteSubject="))
             .build();
 
         Collection<MailAddress> matches = matcher.match(mail);
diff --git a/third-party/rspamd/pom.xml b/third-party/rspamd/pom.xml
index a8a3beb..51142d3 100644
--- a/third-party/rspamd/pom.xml
+++ b/third-party/rspamd/pom.xml
@@ -60,6 +60,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>apache-mailet-base</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>event-bus-api</artifactId>
             <type>test-jar</type>
             <scope>test</scope>
@@ -77,6 +81,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-api</artifactId>
         </dependency>
         <dependency>
@@ -143,4 +151,4 @@
             <scope>test</scope>
         </dependency>
     </dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git a/third-party/rspamd/src/main/java/org/apache/james/rspamd/RSpamDScanner.java b/third-party/rspamd/src/main/java/org/apache/james/rspamd/RSpamDScanner.java
new file mode 100644
index 0000000..4f25e8f
--- /dev/null
+++ b/third-party/rspamd/src/main/java/org/apache/james/rspamd/RSpamDScanner.java
@@ -0,0 +1,83 @@
+/****************************************************************
+ * 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.rspamd;
+
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.mail.MessagingException;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.rspamd.client.RSpamDHttpClient;
+import org.apache.james.rspamd.model.AnalysisResult;
+import org.apache.james.server.core.MimeMessageInputStream;
+import org.apache.mailet.Attribute;
+import org.apache.mailet.AttributeName;
+import org.apache.mailet.AttributeValue;
+import org.apache.mailet.Mail;
+import org.apache.mailet.PerRecipientHeaders;
+import org.apache.mailet.base.GenericMailet;
+
+import com.google.common.collect.ImmutableList;
+
+public class RSpamDScanner extends GenericMailet {
+    public static final AttributeName FLAG_MAIL = AttributeName.of("org.apache.james.rspamd.flag");
+    public static final AttributeName STATUS_MAIL = AttributeName.of("org.apache.james.rspamd.status");
+
+    private final RSpamDHttpClient rSpamDHttpClient;
+
+    @Inject
+    public RSpamDScanner(RSpamDHttpClient rSpamDHttpClient) {
+        this.rSpamDHttpClient = rSpamDHttpClient;
+    }
+
+    @Override
+    public void service(Mail mail) throws MessagingException {
+        AnalysisResult rSpamDResult = rSpamDHttpClient.checkV2(new MimeMessageInputStream(mail.getMessage())).block();
+
+        mail.getRecipients()
+            .forEach(recipient -> appendRSpamDResultHeader(mail, recipient, rSpamDResult));
+    }
+
+    private void appendRSpamDResultHeader(Mail mail, MailAddress recipient, AnalysisResult rSpamDResult) {
+        for (Attribute attribute : getHeadersAsAttributes(rSpamDResult)) {
+            mail.addSpecificHeaderForRecipient(PerRecipientHeaders.Header.builder()
+                .name(attribute.getName().asString())
+                .value((String) attribute.getValue().value())
+                .build(), recipient);
+        }
+    }
+
+    private List<Attribute> getHeadersAsAttributes(AnalysisResult rSpamDResult) {
+        String defaultFlagMailAttributeValue = "NO";
+        String defaultStatusMailAttributeValue = "No";
+        if (rSpamDResult.getAction().equals(AnalysisResult.Action.REJECT)) {
+            defaultFlagMailAttributeValue = "YES";
+            defaultStatusMailAttributeValue = "Yes";
+        }
+
+        return ImmutableList.of(new Attribute(FLAG_MAIL, AttributeValue.of(defaultFlagMailAttributeValue)),
+            new Attribute(STATUS_MAIL, AttributeValue.of(defaultStatusMailAttributeValue + ","
+                + " actions=" + rSpamDResult.getAction().getDescription()
+                + " score=" + rSpamDResult.getScore()
+                + " requiredScore=" + rSpamDResult.getRequiredScore())));
+    }
+}
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/DockerRSpamDExtension.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/DockerRSpamDExtension.java
index 0dc22cc..47fe2b7 100644
--- a/third-party/rspamd/src/test/java/org/apache/james/rspamd/DockerRSpamDExtension.java
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/DockerRSpamDExtension.java
@@ -59,7 +59,11 @@
         return dockerRSpamD();
     }
 
-    public URL getBaseUrl() throws MalformedURLException {
-        return new URL("http://127.0.0.1:" + dockerRSpamD().getPort());
+    public URL getBaseUrl() {
+        try {
+            return new URL("http://127.0.0.1:" + dockerRSpamD().getPort());
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
     }
 }
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/RSpamDScannerTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/RSpamDScannerTest.java
new file mode 100644
index 0000000..3079a84
--- /dev/null
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/RSpamDScannerTest.java
@@ -0,0 +1,151 @@
+/****************************************************************
+ * 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.rspamd;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import javax.mail.internet.MimeMessage;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.rspamd.client.RSpamDClientConfiguration;
+import org.apache.james.rspamd.client.RSpamDHttpClient;
+import org.apache.james.util.MimeMessageUtil;
+import org.apache.mailet.Mail;
+import org.apache.mailet.PerRecipientHeaders;
+import org.apache.mailet.base.test.FakeMail;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.google.common.collect.ImmutableList;
+
+class RSpamDScannerTest {
+
+    @RegisterExtension
+    static DockerRSpamDExtension rSpamDExtension = new DockerRSpamDExtension();
+    static final String rSpamDPassword = "admin";
+
+    private RSpamDScanner mailet;
+
+    @BeforeEach
+    void setup() {
+        RSpamDClientConfiguration configuration = new RSpamDClientConfiguration(rSpamDExtension.getBaseUrl(), rSpamDPassword, Optional.empty());
+        RSpamDHttpClient client = new RSpamDHttpClient(configuration);
+        mailet = new RSpamDScanner(client);
+    }
+
+    @Test
+    void serviceShouldWriteSpamAttributeOnMail() throws Exception {
+        Mail mail = FakeMail.builder()
+            .name("name")
+            .recipient("user1@exemple.com")
+            .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                .addToRecipient("user1@exemple.com")
+                .addFrom("sender@exemple.com")
+                .setSubject("testing")
+                .setText("Please!")
+                .build())
+            .build();
+
+        mailet.service(mail);
+
+        assertThat(
+            mail.getPerRecipientSpecificHeaders()
+                .getHeadersByRecipient()
+                .get(new MailAddress("user1@exemple.com"))
+                .stream()
+                .map(PerRecipientHeaders.Header::getName)
+                .collect(ImmutableList.toImmutableList()))
+            .contains(RSpamDScanner.FLAG_MAIL.asString(), RSpamDScanner.STATUS_MAIL.asString());
+    }
+
+    @Test
+    void serviceShouldWriteMessageAsNotSpamWhenNotSpam() throws Exception {
+        Mail mail = FakeMail.builder()
+            .name("name")
+            .recipient("user1@exemple.com")
+            .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                .addToRecipient("user1@exemple.com")
+                .addFrom("sender@exemple.com")
+                .setSubject("testing")
+                .setText("Please!")
+                .build())
+            .build();
+
+        mailet.service(mail);
+
+        Collection<PerRecipientHeaders.Header> headersForRecipient = mail.getPerRecipientSpecificHeaders()
+            .getHeadersForRecipient(new MailAddress("user1@exemple.com"));
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(headersForRecipient.stream()
+                    .filter(header -> header.getName().equals(RSpamDScanner.FLAG_MAIL.asString()))
+                    .filter(header -> header.getValue().startsWith("NO"))
+                    .findAny())
+                .isPresent();
+
+            softly.assertThat(headersForRecipient.stream()
+                    .filter(header -> header.getName().equals(RSpamDScanner.STATUS_MAIL.asString()))
+                    .filter(header -> header.getValue().startsWith("No, actions=no action"))
+                    .findAny())
+                .isPresent();
+
+        });
+    }
+
+    @Test
+    void serviceShouldWriteMessageAsSpamWhenSpam() throws Exception {
+        MimeMessage mimeMessage = MimeMessageUtil.mimeMessageFromStream(
+            ClassLoader.getSystemResourceAsStream("mail/spam/spam8.eml"));
+
+        Mail mail = FakeMail.builder()
+            .name("name")
+            .recipient("user1@exemple.com")
+            .mimeMessage(mimeMessage)
+            .build();
+
+        mailet.service(mail);
+
+
+        Collection<PerRecipientHeaders.Header> headersForRecipient = mail.getPerRecipientSpecificHeaders()
+            .getHeadersForRecipient(new MailAddress("user1@exemple.com"));
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(headersForRecipient.stream()
+                    .filter(header -> header.getName().equals(RSpamDScanner.FLAG_MAIL.asString()))
+                    .filter(header -> header.getValue().startsWith("YES"))
+                    .findAny())
+                .isPresent();
+
+            softly.assertThat(headersForRecipient.stream()
+                    .filter(header -> header.getName().equals(RSpamDScanner.STATUS_MAIL.asString()))
+                    .filter(header -> header.getValue().startsWith("Yes, actions=reject"))
+                    .findAny())
+                .isPresent();
+
+        });
+    }
+}
\ No newline at end of file