You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2022/08/20 05:54:12 UTC
[james-project] 01/02: JAMES-3775 Write a task to feed ham to RSpamD
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 551927329f04bdf7abe4349db4623d7984813059
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Fri Jul 29 11:36:33 2022 +0700
JAMES-3775 Write a task to feed ham to RSpamD
---
.../james/rspamd/task/FeedHamToRSpamDTask.java | 382 +++++++++++++++++++++
...eedHamToRSpamDTaskAdditionalInformationDTO.java | 106 ++++++
.../rspamd/task/GetMailboxMessagesService.java | 30 ++
...amToRSpamDTaskAdditionalInformationDTOTest.java | 63 ++++
.../james/rspamd/task/FeedHamToRSpamDTaskTest.java | 382 +++++++++++++++++++++
.../feedHamEmptyPeriod.additionalInformation.json | 11 +
...eedHamNonEmptyPeriod.additionalInformation.json | 12 +
7 files changed, 986 insertions(+)
diff --git a/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTask.java b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTask.java
new file mode 100644
index 0000000000..e34208e876
--- /dev/null
+++ b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTask.java
@@ -0,0 +1,382 @@
+/****************************************************************
+ * 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.task;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.rspamd.client.RSpamDHttpClient;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.util.ReactorUtils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.fge.lambdas.Throwing;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import reactor.core.publisher.Mono;
+
+public class FeedHamToRSpamDTask implements Task {
+ public static final TaskType TASK_TYPE = TaskType.of("FeedHamToRSpamDTask");
+
+ public static class RunningOptions {
+ public static final Optional<Long> DEFAULT_PERIOD = Optional.empty();
+ public static final int DEFAULT_MESSAGES_PER_SECOND = 10;
+ public static final double DEFAULT_SAMPLING_PROBABILITY = 1;
+ public static final RunningOptions DEFAULT = new RunningOptions(DEFAULT_PERIOD, DEFAULT_MESSAGES_PER_SECOND,
+ DEFAULT_SAMPLING_PROBABILITY);
+
+ private final Optional<Long> periodInSecond;
+ private final int messagesPerSecond;
+ private final double samplingProbability;
+
+ public RunningOptions(@JsonProperty("periodInSecond") Optional<Long> periodInSecond,
+ @JsonProperty("messagesPerSecond") int messagesPerSecond,
+ @JsonProperty("samplingProbability") double samplingProbability) {
+ this.periodInSecond = periodInSecond;
+ this.messagesPerSecond = messagesPerSecond;
+ this.samplingProbability = samplingProbability;
+ }
+
+ public Optional<Long> getPeriodInSecond() {
+ return periodInSecond;
+ }
+
+ public int getMessagesPerSecond() {
+ return messagesPerSecond;
+ }
+
+ public double getSamplingProbability() {
+ return samplingProbability;
+ }
+ }
+
+ public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+
+ private static AdditionalInformation from(Context context) {
+ Context.Snapshot snapshot = context.snapshot();
+ return new AdditionalInformation(
+ Clock.systemUTC().instant(),
+ snapshot.getHamMessageCount(),
+ snapshot.getReportedHamMessageCount(),
+ snapshot.getErrorCount(),
+ snapshot.getMessagesPerSecond(),
+ snapshot.getPeriod(),
+ snapshot.getSamplingProbability());
+ }
+
+ private final Instant timestamp;
+ private final long hamMessageCount;
+ private final long reportedHamMessageCount;
+ private final long errorCount;
+ private final int messagesPerSecond;
+ private final Optional<Long> period;
+ private final double samplingProbability;
+
+ public AdditionalInformation(Instant timestamp, long hamMessageCount, long reportedHamMessageCount, long errorCount, int messagesPerSecond, Optional<Long> period, double samplingProbability) {
+ this.timestamp = timestamp;
+ this.hamMessageCount = hamMessageCount;
+ this.reportedHamMessageCount = reportedHamMessageCount;
+ this.errorCount = errorCount;
+ this.messagesPerSecond = messagesPerSecond;
+ this.period = period;
+ this.samplingProbability = samplingProbability;
+ }
+
+ public long getHamMessageCount() {
+ return hamMessageCount;
+ }
+
+ public long getReportedHamMessageCount() {
+ return reportedHamMessageCount;
+ }
+
+ public long getErrorCount() {
+ return errorCount;
+ }
+
+ public int getMessagesPerSecond() {
+ return messagesPerSecond;
+ }
+
+ public Optional<Long> getPeriod() {
+ return period;
+ }
+
+ public double getSamplingProbability() {
+ return samplingProbability;
+ }
+
+ @Override
+ public Instant timestamp() {
+ return timestamp;
+ }
+ }
+
+ public static class Context {
+
+ public static class Snapshot {
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ static class Builder {
+ private Optional<Long> hamMessageCount;
+ private Optional<Long> reportedHamMessageCount;
+ private Optional<Long> errorCount;
+ private Optional<Integer> messagesPerSecond;
+ private Optional<Long> period;
+ private Optional<Double> samplingProbability;
+
+ Builder() {
+ hamMessageCount = Optional.empty();
+ reportedHamMessageCount = Optional.empty();
+ errorCount = Optional.empty();
+ messagesPerSecond = Optional.empty();
+ period = Optional.empty();
+ samplingProbability = Optional.empty();
+ }
+
+ public Snapshot build() {
+ return new Snapshot(
+ hamMessageCount.orElse(0L),
+ reportedHamMessageCount.orElse(0L),
+ errorCount.orElse(0L),
+ messagesPerSecond.orElse(0),
+ period,
+ samplingProbability.orElse(1D));
+ }
+
+ public Builder hamMessageCount(long hamMessageCount) {
+ this.hamMessageCount = Optional.of(hamMessageCount);
+ return this;
+ }
+
+ public Builder reportedHamMessageCount(long reportedHamMessageCount) {
+ this.reportedHamMessageCount = Optional.of(reportedHamMessageCount);
+ return this;
+ }
+
+ public Builder errorCount(long errorCount) {
+ this.errorCount = Optional.of(errorCount);
+ return this;
+ }
+
+ public Builder messagesPerSecond(int messagesPerSecond) {
+ this.messagesPerSecond = Optional.of(messagesPerSecond);
+ return this;
+ }
+
+ public Builder period(Optional<Long> period) {
+ this.period = period;
+ return this;
+ }
+
+ public Builder samplingProbability(double samplingProbability) {
+ this.samplingProbability = Optional.of(samplingProbability);
+ return this;
+ }
+ }
+
+ private final long hamMessageCount;
+ private final long reportedHamMessageCount;
+ private final long errorCount;
+ private final int messagesPerSecond;
+ private final Optional<Long> period;
+ private final double samplingProbability;
+
+ public Snapshot(long hamMessageCount, long reportedHamMessageCount, long errorCount, int messagesPerSecond, Optional<Long> period,
+ double samplingProbability) {
+ this.hamMessageCount = hamMessageCount;
+ this.reportedHamMessageCount = reportedHamMessageCount;
+ this.errorCount = errorCount;
+ this.messagesPerSecond = messagesPerSecond;
+ this.period = period;
+ this.samplingProbability = samplingProbability;
+ }
+
+ public long getHamMessageCount() {
+ return hamMessageCount;
+ }
+
+ public long getReportedHamMessageCount() {
+ return reportedHamMessageCount;
+ }
+
+ public long getErrorCount() {
+ return errorCount;
+ }
+
+ public int getMessagesPerSecond() {
+ return messagesPerSecond;
+ }
+
+ public Optional<Long> getPeriod() {
+ return period;
+ }
+
+ public double getSamplingProbability() {
+ return samplingProbability;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof Snapshot) {
+ Snapshot snapshot = (Snapshot) o;
+
+ return Objects.equals(this.hamMessageCount, snapshot.hamMessageCount)
+ && Objects.equals(this.reportedHamMessageCount, snapshot.reportedHamMessageCount)
+ && Objects.equals(this.errorCount, snapshot.errorCount)
+ && Objects.equals(this.messagesPerSecond, snapshot.messagesPerSecond)
+ && Objects.equals(this.samplingProbability, snapshot.samplingProbability)
+ && Objects.equals(this.period, snapshot.period);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(hamMessageCount, reportedHamMessageCount, errorCount, messagesPerSecond, period, samplingProbability);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("hamMessageCount", hamMessageCount)
+ .add("reportedHamMessageCount", reportedHamMessageCount)
+ .add("errorCount", errorCount)
+ .add("messagesPerSecond", messagesPerSecond)
+ .add("period", period)
+ .add("samplingProbability", samplingProbability)
+ .toString();
+ }
+ }
+
+ private final AtomicLong hamMessageCount;
+ private final AtomicLong reportedHamMessageCount;
+ private final AtomicLong errorCount;
+ private final Integer messagesPerSecond;
+ private final Optional<Long> period;
+ private final Double samplingProbability;
+
+ public Context(RunningOptions runningOptions) {
+ this.hamMessageCount = new AtomicLong();
+ this.reportedHamMessageCount = new AtomicLong();
+ this.errorCount = new AtomicLong();
+ this.messagesPerSecond = runningOptions.messagesPerSecond;
+ this.period = runningOptions.periodInSecond;
+ this.samplingProbability = runningOptions.samplingProbability;
+ }
+
+ public void incrementHamMessageCount() {
+ hamMessageCount.incrementAndGet();
+ }
+
+ public void incrementReportedHamMessageCount(int count) {
+ reportedHamMessageCount.addAndGet(count);
+ }
+
+ public void incrementErrorCount() {
+ errorCount.incrementAndGet();
+ }
+
+ public Snapshot snapshot() {
+ return Snapshot.builder()
+ .hamMessageCount(hamMessageCount.get())
+ .reportedHamMessageCount(reportedHamMessageCount.get())
+ .errorCount(errorCount.get())
+ .messagesPerSecond(messagesPerSecond)
+ .period(period)
+ .samplingProbability(samplingProbability)
+ .build();
+ }
+ }
+
+ private final GetMailboxMessagesService messagesService;
+ private final RSpamDHttpClient rSpamDHttpClient;
+ private final RunningOptions runningOptions;
+ private final Context context;
+ private final Clock clock;
+
+ public FeedHamToRSpamDTask(MailboxManager mailboxManager, UsersRepository usersRepository, MessageIdManager messageIdManager, MailboxSessionMapperFactory mapperFactory,
+ RSpamDHttpClient rSpamDHttpClient, RunningOptions runningOptions, Clock clock) {
+ this.runningOptions = runningOptions;
+ this.messagesService = new GetMailboxMessagesService(mailboxManager, usersRepository, mapperFactory, messageIdManager);
+ this.rSpamDHttpClient = rSpamDHttpClient;
+ this.context = new Context(runningOptions);
+ this.clock = clock;
+ }
+
+ @Override
+ public Result run() {
+ Optional<Date> afterDate = runningOptions.periodInSecond.map(periodInSecond -> Date.from(clock.instant().minusSeconds(periodInSecond)));
+ try {
+ return messagesService.getHamMessagesOfAllUser(afterDate, runningOptions.getSamplingProbability(), context)
+ .transform(ReactorUtils.<MessageResult, Result>throttle()
+ .elements(runningOptions.messagesPerSecond)
+ .per(Duration.ofSeconds(1))
+ .forOperation(messageResult -> Mono.fromSupplier(Throwing.supplier(() -> rSpamDHttpClient.reportAsHam(messageResult.getFullContent().getInputStream())))
+ .then(Mono.fromCallable(() -> {
+ context.incrementReportedHamMessageCount(1);
+ return Result.COMPLETED;
+ }))
+ .onErrorResume(error -> {
+ LOGGER.error("Error when report ham message to RSpamD", error);
+ context.incrementErrorCount();
+ return Mono.just(Result.PARTIAL);
+ })))
+ .reduce(Task::combine)
+ .switchIfEmpty(Mono.just(Result.COMPLETED))
+ .block();
+ } catch (UsersRepositoryException e) {
+ LOGGER.error("Error while accessing users from repository", e);
+ return Result.PARTIAL;
+ }
+ }
+
+ @Override
+ public TaskType type() {
+ return TASK_TYPE;
+ }
+
+ @Override
+ public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+ return Optional.of(AdditionalInformation.from(context));
+ }
+
+ @VisibleForTesting
+ public Context.Snapshot snapshot() {
+ return context.snapshot();
+ }
+}
diff --git a/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTO.java b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000000..ae69092e6a
--- /dev/null
+++ b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTO.java
@@ -0,0 +1,106 @@
+/****************************************************************
+ * 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.task;
+
+import java.time.Instant;
+
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class FeedHamToRSpamDTaskAdditionalInformationDTO implements AdditionalInformationDTO {
+ public static final AdditionalInformationDTOModule<FeedHamToRSpamDTask.AdditionalInformation, FeedHamToRSpamDTaskAdditionalInformationDTO> SERIALIZATION_MODULE =
+ DTOModule.forDomainObject(FeedHamToRSpamDTask.AdditionalInformation.class)
+ .convertToDTO(FeedHamToRSpamDTaskAdditionalInformationDTO.class)
+ .toDomainObjectConverter(FeedHamToRSpamDTaskAdditionalInformationDTO::toDomainObject)
+ .toDTOConverter(FeedHamToRSpamDTaskAdditionalInformationDTO::toDto)
+ .typeName(FeedHamToRSpamDTask.TASK_TYPE.asString())
+ .withFactory(AdditionalInformationDTOModule::new);
+
+ private static FeedHamToRSpamDTask.AdditionalInformation toDomainObject(FeedHamToRSpamDTaskAdditionalInformationDTO dto) {
+ return new FeedHamToRSpamDTask.AdditionalInformation(
+ dto.timestamp,
+ dto.hamMessageCount,
+ dto.reportedHamMessageCount,
+ dto.errorCount,
+ dto.runningOptions.getMessagesPerSecond(),
+ dto.runningOptions.getPeriodInSecond(),
+ dto.runningOptions.getSamplingProbability());
+ }
+
+ private static FeedHamToRSpamDTaskAdditionalInformationDTO toDto(FeedHamToRSpamDTask.AdditionalInformation domainObject, String type) {
+ return new FeedHamToRSpamDTaskAdditionalInformationDTO(
+ type,
+ domainObject.timestamp(),
+ domainObject.getHamMessageCount(),
+ domainObject.getReportedHamMessageCount(),
+ domainObject.getErrorCount(),
+ new FeedHamToRSpamDTask.RunningOptions(domainObject.getPeriod(), domainObject.getMessagesPerSecond(), domainObject.getSamplingProbability()));
+ }
+
+ private final String type;
+ private final Instant timestamp;
+ private final long hamMessageCount;
+ private final long reportedHamMessageCount;
+ private final long errorCount;
+ private final FeedHamToRSpamDTask.RunningOptions runningOptions;
+
+ public FeedHamToRSpamDTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+ @JsonProperty("timestamp") Instant timestamp,
+ @JsonProperty("hamMessageCount") long hamMessageCount,
+ @JsonProperty("reportedHamMessageCount") long reportedHamMessageCount,
+ @JsonProperty("errorCount") long errorCount,
+ @JsonProperty("runningOptions") FeedHamToRSpamDTask.RunningOptions runningOptions) {
+ this.type = type;
+ this.timestamp = timestamp;
+ this.hamMessageCount = hamMessageCount;
+ this.reportedHamMessageCount = reportedHamMessageCount;
+ this.errorCount = errorCount;
+ this.runningOptions = runningOptions;
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public Instant getTimestamp() {
+ return timestamp;
+ }
+
+ public long getHamMessageCount() {
+ return hamMessageCount;
+ }
+
+ public long getReportedHamMessageCount() {
+ return reportedHamMessageCount;
+ }
+
+ public long getErrorCount() {
+ return errorCount;
+ }
+
+ public FeedHamToRSpamDTask.RunningOptions getRunningOptions() {
+ return runningOptions;
+ }
+}
diff --git a/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/GetMailboxMessagesService.java b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/GetMailboxMessagesService.java
index abae46efbd..f394f4a5c1 100644
--- a/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/GetMailboxMessagesService.java
+++ b/third-party/rspamd/src/main/java/org/apache/james/rspamd/task/GetMailboxMessagesService.java
@@ -19,6 +19,8 @@
package org.apache.james.rspamd.task;
+import static org.apache.james.rspamd.task.FeedSpamToRSpamDTask.SPAM_MAILBOX_NAME;
+
import java.util.Date;
import java.util.Optional;
@@ -46,6 +48,7 @@ import reactor.core.publisher.Mono;
public class GetMailboxMessagesService {
private static final int UNLIMITED = -1;
+ private static final String TRASH_MAILBOX_NAME = "Trash";
private final MailboxManager mailboxManager;
private final UsersRepository userRepository;
@@ -65,6 +68,14 @@ public class GetMailboxMessagesService {
.flatMap(username -> getMailboxMessagesOfAUser(username, mailboxName, afterDate, samplingProbability, context), ReactorUtils.DEFAULT_CONCURRENCY);
}
+ public Flux<MessageResult> getHamMessagesOfAllUser(Optional<Date> afterDate, double samplingProbability,
+ FeedHamToRSpamDTask.Context context) throws UsersRepositoryException {
+ return Iterators.toFlux(userRepository.list())
+ .flatMap(Throwing.function(username -> Flux.fromIterable(mailboxManager.list(mailboxManager.createSystemSession(username)))
+ .filter(this::hamMailboxesPredicate)
+ .flatMap(mailboxPath -> getMailboxMessagesOfAUser(username, mailboxPath, afterDate, samplingProbability, context), 2)), ReactorUtils.DEFAULT_CONCURRENCY);
+ }
+
private Flux<MessageResult> getMailboxMessagesOfAUser(Username username, String mailboxName, Optional<Date> afterDate,
double samplingProbability, FeedSpamToRSpamDTask.Context context) {
MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
@@ -80,10 +91,29 @@ public class GetMailboxMessagesService {
.flatMapMany(messageIds -> messageIdManager.getMessagesReactive(messageIds, FetchGroup.FULL_CONTENT, mailboxSession));
}
+ private Flux<MessageResult> getMailboxMessagesOfAUser(Username username, MailboxPath mailboxPath, Optional<Date> afterDate,
+ double samplingProbability, FeedHamToRSpamDTask.Context context) {
+ MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
+
+ return Mono.from(mailboxManager.getMailboxReactive(mailboxPath, mailboxSession))
+ .map(Throwing.function(MessageManager::getMailboxEntity))
+ .flatMapMany(Throwing.function(mailbox -> mapperFactory.getMessageMapper(mailboxSession).findInMailboxReactive(mailbox, MessageRange.all(), MessageMapper.FetchType.METADATA, UNLIMITED)))
+ .doOnNext(mailboxMessageMetaData -> context.incrementHamMessageCount())
+ .filter(mailboxMessageMetaData -> afterDate.map(date -> mailboxMessageMetaData.getInternalDate().after(date)).orElse(true))
+ .filter(message -> randomBooleanWithProbability(samplingProbability))
+ .map(Message::getMessageId)
+ .collectList()
+ .flatMapMany(messageIds -> messageIdManager.getMessagesReactive(messageIds, FetchGroup.FULL_CONTENT, mailboxSession));
+ }
+
public static boolean randomBooleanWithProbability(double probability) {
if (probability == 1.0) {
return true;
}
return Math.random() < probability;
}
+
+ private boolean hamMailboxesPredicate(MailboxPath mailboxPath) {
+ return !mailboxPath.getName().equals(SPAM_MAILBOX_NAME) && !mailboxPath.getName().equals(TRASH_MAILBOX_NAME);
+ }
}
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTOTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTOTest.java
new file mode 100644
index 0000000000..be856de645
--- /dev/null
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskAdditionalInformationDTOTest.java
@@ -0,0 +1,63 @@
+/****************************************************************
+ * 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.task;
+
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_MESSAGES_PER_SECOND;
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_PERIOD;
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_SAMPLING_PROBABILITY;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.jupiter.api.Test;
+
+class FeedHamToRSpamDTaskAdditionalInformationDTOTest {
+ @Test
+ void shouldMatchJsonSerializationContractWhenEmptyPeriod() throws Exception {
+ JsonSerializationVerifier.dtoModule(FeedHamToRSpamDTaskAdditionalInformationDTO.SERIALIZATION_MODULE)
+ .bean(new FeedHamToRSpamDTask.AdditionalInformation(
+ Instant.parse("2007-12-03T10:15:30.00Z"),
+ 4,
+ 2,
+ 1,
+ DEFAULT_MESSAGES_PER_SECOND,
+ DEFAULT_PERIOD,
+ DEFAULT_SAMPLING_PROBABILITY))
+ .json(ClassLoaderUtils.getSystemResourceAsString("json/feedHamEmptyPeriod.additionalInformation.json"))
+ .verify();
+ }
+
+ @Test
+ void shouldMatchJsonSerializationContractWhenNonEmptyPeriod() throws Exception {
+ JsonSerializationVerifier.dtoModule(FeedHamToRSpamDTaskAdditionalInformationDTO.SERIALIZATION_MODULE)
+ .bean(new FeedHamToRSpamDTask.AdditionalInformation(
+ Instant.parse("2007-12-03T10:15:30.00Z"),
+ 4,
+ 2,
+ 1,
+ DEFAULT_MESSAGES_PER_SECOND,
+ Optional.of(3600L),
+ DEFAULT_SAMPLING_PROBABILITY))
+ .json(ClassLoaderUtils.getSystemResourceAsString("json/feedHamNonEmptyPeriod.additionalInformation.json"))
+ .verify();
+ }
+}
diff --git a/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskTest.java b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskTest.java
new file mode 100644
index 0000000000..c5905d809b
--- /dev/null
+++ b/third-party/rspamd/src/test/java/org/apache/james/rspamd/task/FeedHamToRSpamDTaskTest.java
@@ -0,0 +1,382 @@
+/****************************************************************
+ * 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.task;
+
+import static org.apache.james.rspamd.DockerRSpamD.PASSWORD;
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_MESSAGES_PER_SECOND;
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_PERIOD;
+import static org.apache.james.rspamd.task.FeedHamToRSpamDTask.RunningOptions.DEFAULT_SAMPLING_PROBABILITY;
+import static org.apache.james.rspamd.task.FeedSpamToRSpamDTaskTest.BOB_SPAM_MAILBOX;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+import java.io.ByteArrayInputStream;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.Optional;
+import java.util.stream.IntStream;
+
+import javax.mail.Flags;
+
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.rspamd.DockerRSpamDExtension;
+import org.apache.james.rspamd.client.RSpamDClientConfiguration;
+import org.apache.james.rspamd.client.RSpamDHttpClient;
+import org.apache.james.task.Task;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.james.utils.UpdatableTickingClock;
+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 org.mockito.Mockito;
+
+import com.github.fge.lambdas.Throwing;
+
+public class FeedHamToRSpamDTaskTest {
+ @RegisterExtension
+ static DockerRSpamDExtension rSpamDExtension = new DockerRSpamDExtension();
+
+ public static final String INBOX_MAILBOX_NAME = "INBOX";
+ public static final Domain DOMAIN = Domain.of("domain.tld");
+ public static final Username BOB = Username.fromLocalPartWithDomain("bob", DOMAIN);
+ public static final Username ALICE = Username.fromLocalPartWithDomain("alice", DOMAIN);
+ public static final MailboxPath BOB_INBOX_MAILBOX = MailboxPath.forUser(BOB, INBOX_MAILBOX_NAME);
+ public static final MailboxPath BOB_CUSTOM_MAILBOX = MailboxPath.forUser(BOB, "Custom");
+ public static final MailboxPath BOB_TRASH_MAILBOX = MailboxPath.forUser(BOB, "Trash");
+ public static final MailboxPath ALICE_INBOX_MAILBOX = MailboxPath.forUser(ALICE, INBOX_MAILBOX_NAME);
+ public static final long THREE_DAYS_IN_SECOND = 259200;
+ public static final long TWO_DAYS_IN_SECOND = 172800;
+ public static final long ONE_DAY_IN_SECOND = 86400;
+ public static final Instant NOW = ZonedDateTime.now().toInstant();
+
+ private InMemoryMailboxManager mailboxManager;
+ private MessageIdManager messageIdManager;
+ private MailboxSessionMapperFactory mapperFactory;
+ private UsersRepository usersRepository;
+ private Clock clock;
+ private RSpamDHttpClient client;
+ private FeedHamToRSpamDTask task;
+
+ @BeforeEach
+ void setup() throws Exception {
+ InMemoryIntegrationResources inMemoryIntegrationResources = InMemoryIntegrationResources.defaultResources();
+ mailboxManager = inMemoryIntegrationResources.getMailboxManager();
+ DomainList domainList = mock(DomainList.class);
+ Mockito.when(domainList.containsDomain(any())).thenReturn(true);
+ usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+ usersRepository.addUser(BOB, "anyPassword");
+ usersRepository.addUser(ALICE, "anyPassword");
+ mailboxManager.createMailbox(BOB_INBOX_MAILBOX, mailboxManager.createSystemSession(BOB));
+ mailboxManager.createMailbox(BOB_CUSTOM_MAILBOX, mailboxManager.createSystemSession(BOB));
+ mailboxManager.createMailbox(BOB_TRASH_MAILBOX, mailboxManager.createSystemSession(BOB));
+ mailboxManager.createMailbox(BOB_SPAM_MAILBOX, mailboxManager.createSystemSession(BOB));
+ mailboxManager.createMailbox(ALICE_INBOX_MAILBOX, mailboxManager.createSystemSession(ALICE));
+
+ clock = new UpdatableTickingClock(NOW);
+ client = new RSpamDHttpClient(new RSpamDClientConfiguration(rSpamDExtension.getBaseUrl(), PASSWORD, Optional.empty()));
+ messageIdManager = inMemoryIntegrationResources.getMessageIdManager();
+ mapperFactory = mailboxManager.getMapperFactory();
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, FeedHamToRSpamDTask.RunningOptions.DEFAULT, clock);
+ }
+
+ @Test
+ void shouldReturnDefaultInformationWhenDataIsEmpty() {
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(0)
+ .reportedHamMessageCount(0)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void taskShouldReportAllHamMessagesOfAllUsersByDefault() throws MailboxException {
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW));
+ appendHamMessage(ALICE_INBOX_MAILBOX, Date.from(NOW));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(2)
+ .reportedHamMessageCount(2)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void taskShouldReportHamMessageInPeriod() throws MailboxException {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.of(TWO_DAYS_IN_SECOND),
+ DEFAULT_MESSAGES_PER_SECOND, DEFAULT_SAMPLING_PROBABILITY);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(1)
+ .reportedHamMessageCount(1)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(Optional.of(TWO_DAYS_IN_SECOND))
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void taskShouldNotReportHamMessageNotInPeriod() throws MailboxException {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.of(TWO_DAYS_IN_SECOND),
+ DEFAULT_MESSAGES_PER_SECOND, DEFAULT_SAMPLING_PROBABILITY);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(THREE_DAYS_IN_SECOND)));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(1)
+ .reportedHamMessageCount(0)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(Optional.of(TWO_DAYS_IN_SECOND))
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void mixedInternalDateCase() throws MailboxException {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.of(TWO_DAYS_IN_SECOND),
+ DEFAULT_MESSAGES_PER_SECOND, DEFAULT_SAMPLING_PROBABILITY);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(THREE_DAYS_IN_SECOND)));
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(2)
+ .reportedHamMessageCount(1)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(Optional.of(TWO_DAYS_IN_SECOND))
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void taskWithSamplingProbabilityIsZeroShouldReportNonHamMessage() {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.empty(),
+ DEFAULT_MESSAGES_PER_SECOND, 0);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ IntStream.range(0, 10)
+ .forEach(Throwing.intConsumer(any -> appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)))));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(10)
+ .reportedHamMessageCount(0)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(0)
+ .build());
+ }
+
+ @Test
+ void taskWithDefaultSamplingProbabilityShouldReportAllHamMessages() {
+ IntStream.range(0, 10)
+ .forEach(Throwing.intConsumer(any -> appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)))));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(10)
+ .reportedHamMessageCount(10)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void taskWithVeryLowSamplingProbabilityShouldReportNotAllHamMessages() {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.empty(),
+ DEFAULT_MESSAGES_PER_SECOND, 0.01);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ IntStream.range(0, 10)
+ .forEach(Throwing.intConsumer(any -> appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)))));
+
+ Task.Result result = task.run();
+
+ SoftAssertions.assertSoftly(softly -> {
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot().getHamMessageCount()).isEqualTo(10);
+ assertThat(task.snapshot().getReportedHamMessageCount()).isLessThan(10);
+ assertThat(task.snapshot().getErrorCount()).isZero();
+ });
+ }
+
+ @Test
+ void taskWithVeryHighSamplingProbabilityShouldReportMoreThanZeroMessage() {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.empty(),
+ DEFAULT_MESSAGES_PER_SECOND, 0.99);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ IntStream.range(0, 10)
+ .forEach(Throwing.intConsumer(any -> appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)))));
+
+ Task.Result result = task.run();
+
+ SoftAssertions.assertSoftly(softly -> {
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot().getHamMessageCount()).isEqualTo(10);
+ assertThat(task.snapshot().getReportedHamMessageCount()).isPositive();
+ assertThat(task.snapshot().getErrorCount()).isZero();
+ });
+ }
+
+ @Test
+ void taskWithAverageSamplingProbabilityShouldReportSomeMessages() {
+ FeedHamToRSpamDTask.RunningOptions runningOptions = new FeedHamToRSpamDTask.RunningOptions(Optional.empty(),
+ DEFAULT_MESSAGES_PER_SECOND, 0.5);
+ task = new FeedHamToRSpamDTask(mailboxManager, usersRepository, messageIdManager, mapperFactory, client, runningOptions, clock);
+
+ IntStream.range(0, 10)
+ .forEach(Throwing.intConsumer(any -> appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW.minusSeconds(ONE_DAY_IN_SECOND)))));
+
+ Task.Result result = task.run();
+
+ SoftAssertions.assertSoftly(softly -> {
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot().getHamMessageCount()).isEqualTo(10);
+ assertThat(task.snapshot().getReportedHamMessageCount()).isBetween(1L, 9L); // skip 0 and 10 cases cause their probability is very low (0.5^10)
+ assertThat(task.snapshot().getErrorCount()).isZero();
+ });
+ }
+
+ @Test
+ void shouldNotReportMessagesInTrashAndSpamMailboxes() throws MailboxException {
+ appendHamMessage(BOB_TRASH_MAILBOX, Date.from(NOW));
+ appendHamMessage(BOB_SPAM_MAILBOX, Date.from(NOW));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(0)
+ .reportedHamMessageCount(0)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void shouldReportMessagesInHamMailboxes() throws MailboxException {
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW));
+ appendHamMessage(BOB_CUSTOM_MAILBOX, Date.from(NOW));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(2)
+ .reportedHamMessageCount(2)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ @Test
+ void mixedMailboxesCase() throws MailboxException {
+ appendHamMessage(BOB_INBOX_MAILBOX, Date.from(NOW));
+ appendHamMessage(BOB_CUSTOM_MAILBOX, Date.from(NOW));
+ appendHamMessage(BOB_TRASH_MAILBOX, Date.from(NOW));
+ appendHamMessage(BOB_SPAM_MAILBOX, Date.from(NOW));
+
+ Task.Result result = task.run();
+
+ assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ assertThat(task.snapshot())
+ .isEqualTo(FeedHamToRSpamDTask.Context.Snapshot.builder()
+ .hamMessageCount(2)
+ .reportedHamMessageCount(2)
+ .errorCount(0)
+ .messagesPerSecond(DEFAULT_MESSAGES_PER_SECOND)
+ .period(DEFAULT_PERIOD)
+ .samplingProbability(DEFAULT_SAMPLING_PROBABILITY)
+ .build());
+ }
+
+ private void appendHamMessage(MailboxPath mailboxPath, Date internalDate) throws MailboxException {
+ MailboxSession session = mailboxManager.createSystemSession(mailboxPath.getUser());
+ mailboxManager.getMailbox(mailboxPath, session)
+ .appendMessage(new ByteArrayInputStream(String.format("random content %4.3f", Math.random()).getBytes()),
+ internalDate,
+ session,
+ true,
+ new Flags());
+ }
+}
diff --git a/third-party/rspamd/src/test/resources/json/feedHamEmptyPeriod.additionalInformation.json b/third-party/rspamd/src/test/resources/json/feedHamEmptyPeriod.additionalInformation.json
new file mode 100644
index 0000000000..dc3faae270
--- /dev/null
+++ b/third-party/rspamd/src/test/resources/json/feedHamEmptyPeriod.additionalInformation.json
@@ -0,0 +1,11 @@
+{
+ "errorCount": 1,
+ "reportedHamMessageCount": 2,
+ "runningOptions": {
+ "messagesPerSecond": 10,
+ "samplingProbability": 1.0
+ },
+ "hamMessageCount": 4,
+ "timestamp": "2007-12-03T10:15:30Z",
+ "type": "FeedHamToRSpamDTask"
+}
\ No newline at end of file
diff --git a/third-party/rspamd/src/test/resources/json/feedHamNonEmptyPeriod.additionalInformation.json b/third-party/rspamd/src/test/resources/json/feedHamNonEmptyPeriod.additionalInformation.json
new file mode 100644
index 0000000000..bcdaf02c88
--- /dev/null
+++ b/third-party/rspamd/src/test/resources/json/feedHamNonEmptyPeriod.additionalInformation.json
@@ -0,0 +1,12 @@
+{
+ "errorCount": 1,
+ "reportedHamMessageCount": 2,
+ "runningOptions": {
+ "messagesPerSecond": 10,
+ "periodInSecond": 3600,
+ "samplingProbability": 1.0
+ },
+ "hamMessageCount": 4,
+ "timestamp": "2007-12-03T10:15:30Z",
+ "type": "FeedHamToRSpamDTask"
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org