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