This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch 3.9.x
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit a03e06e26e8b423a8107ad660f591141230c6429
Author: Quan Tran <[email protected]>
AuthorDate: Tue Sep 30 11:12:17 2025 +0700

    JAMES-4148 JMAPFiltering mailet should support moveTo action
---
 .../mailets/FilterForwardIntegrationTest.java      | 197 -------------
 .../james/mailets/FilterIntegrationTest.java       | 311 +++++++++++++++++++++
 .../james/jmap/mailet/filter/ActionApplier.java    |  28 +-
 3 files changed, 336 insertions(+), 200 deletions(-)

diff --git 
a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterForwardIntegrationTest.java
 
b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterForwardIntegrationTest.java
deleted file mode 100644
index d5bde11b11..0000000000
--- 
a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterForwardIntegrationTest.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/****************************************************************
- * 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.mailets;
-
-import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.CONTAINS;
-import static 
org.apache.james.jmap.api.filtering.Rule.Condition.FixedField.FROM;
-import static 
org.apache.james.mailets.configuration.CommonProcessors.RRT_ERROR_REPOSITORY;
-import static org.apache.james.mailets.configuration.Constants.DEFAULT_DOMAIN;
-import static org.apache.james.mailets.configuration.Constants.LOCALHOST_IP;
-import static org.apache.james.mailets.configuration.Constants.PASSWORD;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.io.File;
-
-import org.apache.james.core.Username;
-import org.apache.james.jmap.api.filtering.Rule;
-import org.apache.james.jmap.api.filtering.Rule.Action;
-import org.apache.james.jmap.api.filtering.Rule.Action.Forward;
-import org.apache.james.jmap.mailet.filter.JMAPFiltering;
-import org.apache.james.mailets.configuration.CommonProcessors;
-import org.apache.james.mailets.configuration.MailetConfiguration;
-import org.apache.james.mailets.configuration.MailetContainer;
-import org.apache.james.mailets.configuration.ProcessorConfiguration;
-import org.apache.james.mailrepository.api.MailRepositoryUrl;
-import org.apache.james.modules.protocols.SmtpGuiceProbe;
-import org.apache.james.probe.DataProbe;
-import org.apache.james.transport.mailets.ToRepository;
-import org.apache.james.transport.matchers.All;
-import org.apache.james.utils.DataProbeImpl;
-import org.apache.james.utils.FilteringManagementProbeImpl;
-import org.apache.james.utils.GuiceProbe;
-import org.apache.james.utils.MailRepositoryProbeImpl;
-import org.apache.james.utils.SMTPMessageSender;
-import org.apache.mailet.Mail;
-import org.assertj.core.api.SoftAssertions;
-import org.awaitility.Awaitility;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.io.TempDir;
-
-import com.github.fge.lambdas.Throwing;
-import com.google.inject.multibindings.Multibinder;
-
-public class FilterForwardIntegrationTest {
-
-    private static final Username ALICE = Username.of("alice@" + 
DEFAULT_DOMAIN);
-    public static final Rule.ConditionGroup CONDITION_GROUP = 
Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, Rule.Condition.of(FROM, 
CONTAINS, ALICE.asString()));
-    private static final Username BOB = Username.of("bob@" + DEFAULT_DOMAIN);
-    private static final Username CEDRIC = Username.of("cedric@" + 
DEFAULT_DOMAIN);
-    private static final MailRepositoryUrl CUSTOM_REPOSITORY = 
MailRepositoryUrl.from("memory://var/mail/custom/");
-
-    private static Rule.Builder asRule(Action.Forward forward) {
-        return Rule.builder()
-            .id(Rule.Id.of("1"))
-            .name("rule 1")
-            .conditionGroup(CONDITION_GROUP)
-            .action(Action.builder().setForward(forward));
-    }
-
-    private TemporaryJamesServer jamesServer;
-    private FilteringManagementProbeImpl filteringManagementProbe;
-    private MailRepositoryProbeImpl mailRepositoryProbe;
-
-    @RegisterExtension
-    public SMTPMessageSender messageSender = new 
SMTPMessageSender(DEFAULT_DOMAIN);
-
-    @BeforeEach
-    void setup(@TempDir File temporaryFolder) throws Exception {
-        jamesServer = TemporaryJamesServer.builder()
-            .withOverrides(binder -> Multibinder.newSetBinder(binder, 
GuiceProbe.class).addBinding().to(FilteringManagementProbeImpl.class))
-            .withMailetContainer(MailetContainer.builder()
-                .putProcessor(ProcessorConfiguration.root()
-                    .addMailet(MailetConfiguration.builder()
-                        .matcher(All.class)
-                        .mailet(JMAPFiltering.class))
-                    .addMailet(MailetConfiguration.builder()
-                        .matcher(All.class)
-                        .mailet(ToRepository.class)
-                        .addProperty("repositoryPath", 
CUSTOM_REPOSITORY.asString())))
-                .putProcessor(CommonProcessors.error())
-                .putProcessor(CommonProcessors.rrtError())
-                .putProcessor(CommonProcessors.transport())
-                .putProcessor(CommonProcessors.bounces()))
-            .build(temporaryFolder);
-
-        jamesServer.start();
-
-        DataProbe dataProbe = jamesServer.getProbe(DataProbeImpl.class);
-        dataProbe.addDomain(DEFAULT_DOMAIN);
-
-        dataProbe.addUser(ALICE.asString(), PASSWORD);
-        dataProbe.addUser(BOB.asString(), PASSWORD);
-        dataProbe.addUser(CEDRIC.asString(), PASSWORD);
-
-        mailRepositoryProbe = 
jamesServer.getProbe(MailRepositoryProbeImpl.class);
-
-        filteringManagementProbe = 
jamesServer.getProbe(FilteringManagementProbeImpl.class);
-    }
-
-    @AfterEach
-    void tearDown() {
-        jamesServer.shutdown();
-    }
-
-    @Test
-    void forwardShouldWork() throws Exception {
-        filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Forward.to(CEDRIC.asMailAddress()).keepACopy()));
-
-        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-            .authenticate(ALICE.asString(), PASSWORD)
-            .sendMessage(ALICE.asString(), BOB.asString());
-
-        Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 2L);
-
-        SoftAssertions.assertSoftly(Throwing.consumer(softly -> {
-            Mail mail1 = mailRepositoryProbe.listMails(CUSTOM_REPOSITORY, 
BOB.asMailAddress()).get(0);
-            
softly.assertThat(mail1.getRecipients()).containsOnly(BOB.asMailAddress());
-            
softly.assertThat(mail1.getMaybeSender().asOptional()).contains(ALICE.asMailAddress());
-
-            Mail mail2 = mailRepositoryProbe.listMails(CUSTOM_REPOSITORY, 
CEDRIC.asMailAddress()).get(0);
-            
softly.assertThat(mail2.getRecipients()).containsOnly(CEDRIC.asMailAddress());
-            
softly.assertThat(mail2.getMaybeSender().asOptional()).contains(BOB.asMailAddress());
-        }));
-    }
-
-    @Test
-    void forwardShouldNotKeepACopyWhenKeepACopyIsFalse() throws Exception {
-        filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Forward.to(CEDRIC.asMailAddress()).withoutACopy()));
-
-        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-            .authenticate(ALICE.asString(), PASSWORD)
-            .sendMessage(ALICE.asString(), BOB.asString());
-
-        Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
-
-        SoftAssertions.assertSoftly(Throwing.consumer(softly -> 
softly.assertThat(mailRepositoryProbe.listMails(CUSTOM_REPOSITORY)
-            .anyMatch(Throwing.predicate(mail -> 
mail.getRecipients().contains(BOB.asMailAddress())))).isFalse()));
-    }
-
-    @Test
-    void regularForwardShouldNotLeadToRecordingRRTError() throws Exception {
-        filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Forward.to(CEDRIC.asMailAddress()).withoutACopy()));
-
-        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-            .authenticate(ALICE.asString(), PASSWORD)
-            .sendMessage(ALICE.asString(), BOB.asString());
-
-        Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
-
-        
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
-    }
-
-    @Test
-    void localCopyShouldNotLeadToRecordingRRTError() throws Exception {
-        filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Forward.to(CEDRIC.asMailAddress()).keepACopy()));
-
-        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-            .authenticate(ALICE.asString(), PASSWORD)
-            .sendMessage(ALICE.asString(), BOB.asString());
-
-        Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 2L);
-
-        
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
-    }
-
-    @Test
-    void localCopyAloneShouldNotLeadToRecordingRRTError() throws Exception {
-        filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Forward.to().keepACopy()));
-
-        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
-            .authenticate(ALICE.asString(), PASSWORD)
-            .sendMessage(ALICE.asString(), BOB.asString());
-
-        Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
-
-        
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
-    }
-}
diff --git 
a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java
 
b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java
new file mode 100644
index 0000000000..6901446449
--- /dev/null
+++ 
b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java
@@ -0,0 +1,311 @@
+/****************************************************************
+ * 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.mailets;
+
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.CONTAINS;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.FixedField.FROM;
+import static 
org.apache.james.mailets.configuration.CommonProcessors.RRT_ERROR_REPOSITORY;
+import static org.apache.james.mailets.configuration.Constants.DEFAULT_DOMAIN;
+import static org.apache.james.mailets.configuration.Constants.LOCALHOST_IP;
+import static org.apache.james.mailets.configuration.Constants.PASSWORD;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.jmap.api.filtering.Rule.Action;
+import org.apache.james.jmap.api.filtering.Rule.Action.Forward;
+import org.apache.james.jmap.mailet.filter.JMAPFiltering;
+import org.apache.james.mailbox.DefaultMailboxes;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailets.configuration.CommonProcessors;
+import org.apache.james.mailets.configuration.Constants;
+import org.apache.james.mailets.configuration.MailetConfiguration;
+import org.apache.james.mailets.configuration.MailetContainer;
+import org.apache.james.mailets.configuration.ProcessorConfiguration;
+import org.apache.james.mailrepository.api.MailRepositoryUrl;
+import org.apache.james.modules.MailboxProbeImpl;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.modules.protocols.SmtpGuiceProbe;
+import org.apache.james.probe.DataProbe;
+import org.apache.james.transport.mailets.ToProcessor;
+import org.apache.james.transport.mailets.ToRepository;
+import org.apache.james.transport.matchers.All;
+import org.apache.james.utils.DataProbeImpl;
+import org.apache.james.utils.FilteringManagementProbeImpl;
+import org.apache.james.utils.GuiceProbe;
+import org.apache.james.utils.MailRepositoryProbeImpl;
+import org.apache.james.utils.SMTPMessageSender;
+import org.apache.james.utils.TestIMAPClient;
+import org.apache.mailet.Mail;
+import org.assertj.core.api.SoftAssertions;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.inject.multibindings.Multibinder;
+
+public class FilterIntegrationTest {
+
+    private static final Username ALICE = Username.of("alice@" + 
DEFAULT_DOMAIN);
+    public static final Rule.ConditionGroup CONDITION_GROUP = 
Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, Rule.Condition.of(FROM, 
CONTAINS, ALICE.asString()));
+    private static final Username BOB = Username.of("bob@" + DEFAULT_DOMAIN);
+    private static final Username CEDRIC = Username.of("cedric@" + 
DEFAULT_DOMAIN);
+    private static final MailRepositoryUrl CUSTOM_REPOSITORY = 
MailRepositoryUrl.from("memory://var/mail/custom/");
+
+    private static Rule.Builder asRule(Action.Builder actionBuilder) {
+        return Rule.builder()
+            .id(Rule.Id.of("1"))
+            .name("rule 1")
+            .conditionGroup(CONDITION_GROUP)
+            .action(actionBuilder);
+    }
+
+    private TemporaryJamesServer jamesServer;
+    private FilteringManagementProbeImpl filteringManagementProbe;
+    private MailRepositoryProbeImpl mailRepositoryProbe;
+
+    @RegisterExtension
+    public SMTPMessageSender messageSender = new 
SMTPMessageSender(DEFAULT_DOMAIN);
+
+    @RegisterExtension
+    public TestIMAPClient imapClient = new TestIMAPClient();
+
+    @Nested
+    class ForwardTests {
+        @BeforeEach
+        void setup(@TempDir File temporaryFolder) throws Exception {
+            jamesServer = TemporaryJamesServer.builder()
+                .withOverrides(binder -> Multibinder.newSetBinder(binder, 
GuiceProbe.class).addBinding().to(FilteringManagementProbeImpl.class))
+                .withMailetContainer(MailetContainer.builder()
+                    .putProcessor(ProcessorConfiguration.root()
+                        .addMailet(MailetConfiguration.builder()
+                            .matcher(All.class)
+                            .mailet(JMAPFiltering.class))
+                        .addMailet(MailetConfiguration.builder()
+                            .matcher(All.class)
+                            .mailet(ToRepository.class)
+                            .addProperty("repositoryPath", 
CUSTOM_REPOSITORY.asString())))
+                    .putProcessor(CommonProcessors.error())
+                    .putProcessor(CommonProcessors.rrtError())
+                    .putProcessor(CommonProcessors.transport())
+                    .putProcessor(CommonProcessors.bounces()))
+                .build(temporaryFolder);
+
+            jamesServer.start();
+
+            DataProbe dataProbe = jamesServer.getProbe(DataProbeImpl.class);
+            dataProbe.addDomain(DEFAULT_DOMAIN);
+
+            dataProbe.addUser(ALICE.asString(), PASSWORD);
+            dataProbe.addUser(BOB.asString(), PASSWORD);
+            dataProbe.addUser(CEDRIC.asString(), PASSWORD);
+
+            mailRepositoryProbe = 
jamesServer.getProbe(MailRepositoryProbeImpl.class);
+
+            filteringManagementProbe = 
jamesServer.getProbe(FilteringManagementProbeImpl.class);
+        }
+
+        @AfterEach
+        void tearDown() {
+            jamesServer.shutdown();
+        }
+
+        @Test
+        void forwardShouldWork() throws Exception {
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder().setForward(Forward.to(CEDRIC.asMailAddress()).keepACopy())));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 2L);
+
+            SoftAssertions.assertSoftly(Throwing.consumer(softly -> {
+                Mail mail1 = mailRepositoryProbe.listMails(CUSTOM_REPOSITORY, 
BOB.asMailAddress()).get(0);
+                
softly.assertThat(mail1.getRecipients()).containsOnly(BOB.asMailAddress());
+                
softly.assertThat(mail1.getMaybeSender().asOptional()).contains(ALICE.asMailAddress());
+
+                Mail mail2 = mailRepositoryProbe.listMails(CUSTOM_REPOSITORY, 
CEDRIC.asMailAddress()).get(0);
+                
softly.assertThat(mail2.getRecipients()).containsOnly(CEDRIC.asMailAddress());
+                
softly.assertThat(mail2.getMaybeSender().asOptional()).contains(BOB.asMailAddress());
+            }));
+        }
+
+        @Test
+        void forwardShouldNotKeepACopyWhenKeepACopyIsFalse() throws Exception {
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder().setForward(Forward.to(CEDRIC.asMailAddress()).withoutACopy())));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
+
+            SoftAssertions.assertSoftly(Throwing.consumer(softly -> 
softly.assertThat(mailRepositoryProbe.listMails(CUSTOM_REPOSITORY)
+                .anyMatch(Throwing.predicate(mail -> 
mail.getRecipients().contains(BOB.asMailAddress())))).isFalse()));
+        }
+
+        @Test
+        void regularForwardShouldNotLeadToRecordingRRTError() throws Exception 
{
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder().setForward(Forward.to(CEDRIC.asMailAddress()).withoutACopy())));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
+
+            
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
+        }
+
+        @Test
+        void localCopyShouldNotLeadToRecordingRRTError() throws Exception {
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder().setForward(Forward.to(CEDRIC.asMailAddress()).keepACopy())));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 2L);
+
+            
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
+        }
+
+        @Test
+        void localCopyAloneShouldNotLeadToRecordingRRTError() throws Exception 
{
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder().setForward(Forward.to().keepACopy())));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            Awaitility.await().until(() -> 
mailRepositoryProbe.getRepositoryMailCount(CUSTOM_REPOSITORY) == 1L);
+
+            
assertThat(mailRepositoryProbe.getRepositoryMailCount(RRT_ERROR_REPOSITORY)).isZero();
+        }
+    }
+
+    @Nested
+    class MoveToTests {
+        @BeforeEach
+        void setup(@TempDir File temporaryFolder) throws Exception {
+            jamesServer = TemporaryJamesServer.builder()
+                .withOverrides(binder -> Multibinder.newSetBinder(binder, 
GuiceProbe.class).addBinding().to(FilteringManagementProbeImpl.class))
+                .withMailetContainer(MailetContainer.builder()
+                    .putProcessor(ProcessorConfiguration.root()
+                        .addMailet(MailetConfiguration.builder()
+                            .matcher(All.class)
+                            .mailet(JMAPFiltering.class))
+                        .addMailet(MailetConfiguration.builder()
+                            .matcher(All.class)
+                            .mailet(ToProcessor.class)
+                            .addProperty("processor", "transport")))
+                    .putProcessor(CommonProcessors.error())
+                    .putProcessor(CommonProcessors.rrtError())
+                    .putProcessor(CommonProcessors.transport())
+                    .putProcessor(CommonProcessors.bounces()))
+                .build(temporaryFolder);
+
+            jamesServer.start();
+
+            DataProbe dataProbe = jamesServer.getProbe(DataProbeImpl.class);
+            dataProbe.addDomain(DEFAULT_DOMAIN);
+
+            dataProbe.addUser(ALICE.asString(), PASSWORD);
+            dataProbe.addUser(BOB.asString(), PASSWORD);
+
+            mailRepositoryProbe = 
jamesServer.getProbe(MailRepositoryProbeImpl.class);
+            filteringManagementProbe = 
jamesServer.getProbe(FilteringManagementProbeImpl.class);
+        }
+
+        @AfterEach
+        void tearDown() {
+            jamesServer.shutdown();
+        }
+
+        @Test
+        void moveToShouldWork() throws Exception {
+            MailboxProbeImpl mailboxProbe = 
jamesServer.getProbe(MailboxProbeImpl.class);
+            mailboxProbe.createMailbox("#private", BOB.asString(), 
DefaultMailboxes.TRASH);
+
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder()
+                .setMoveTo(Optional.of(new 
Action.MoveTo(DefaultMailboxes.TRASH)))));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            imapClient.connect(LOCALHOST_IP, 
jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+                .login(BOB, PASSWORD)
+                .select(DefaultMailboxes.TRASH)
+                .awaitMessageCount(Constants.awaitAtMostOneMinute, 1);
+        }
+
+        @Test
+        void moveToWithAppendInShouldWork() throws Exception {
+            MailboxProbeImpl mailboxProbe = 
jamesServer.getProbe(MailboxProbeImpl.class);
+            MailboxId archiveMailboxId = 
mailboxProbe.createMailbox("#private", BOB.asString(), 
DefaultMailboxes.ARCHIVE);
+            mailboxProbe.createMailbox("#private", BOB.asString(), 
DefaultMailboxes.TRASH);
+
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder()
+                
.setAppendInMailboxes(Action.AppendInMailboxes.withMailboxIds(archiveMailboxId.serialize()))
+                .setMoveTo(Optional.of(new 
Action.MoveTo(DefaultMailboxes.TRASH)))));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            imapClient.connect(LOCALHOST_IP, 
jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+                .login(BOB, PASSWORD)
+                .select(DefaultMailboxes.TRASH)
+                .awaitMessageCount(Constants.awaitAtMostOneMinute, 1)
+                .select(DefaultMailboxes.ARCHIVE)
+                .awaitMessageCount(Constants.awaitAtMostOneMinute, 1);
+        }
+
+        @Test
+        void moveToWithAppendInTheSameMailboxShouldNotDuplicateMails() throws 
Exception {
+            MailboxProbeImpl mailboxProbe = 
jamesServer.getProbe(MailboxProbeImpl.class);
+            MailboxId archiveMailboxId = 
mailboxProbe.createMailbox("#private", BOB.asString(), 
DefaultMailboxes.ARCHIVE);
+
+            filteringManagementProbe.defineRulesForUser(BOB, 
asRule(Action.builder()
+                
.setAppendInMailboxes(Action.AppendInMailboxes.withMailboxIds(archiveMailboxId.serialize()))
+                .setMoveTo(Optional.of(new 
Action.MoveTo(DefaultMailboxes.ARCHIVE)))));
+
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(ALICE.asString(), PASSWORD)
+                .sendMessage(ALICE.asString(), BOB.asString());
+
+            imapClient.connect(LOCALHOST_IP, 
jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+                .login(BOB, PASSWORD)
+                .select(DefaultMailboxes.ARCHIVE)
+                .awaitMessageCount(Constants.awaitAtMostOneMinute, 1);
+        }
+    }
+
+}
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
 
b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
index b5adf822c4..cca4fa0b00 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
+++ 
b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
@@ -39,6 +39,7 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.server.core.MailImpl;
 import org.apache.james.util.AuditTrail;
 import org.apache.mailet.LoopPrevention;
@@ -198,9 +199,14 @@ public class ActionApplier {
     }
 
     private Optional<ImmutableList<String>> computeTargetMailboxes(Rule.Action 
action) {
-        return Optional.of(action.getAppendInMailboxes().getMailboxIds()
-                .stream()
-                .flatMap(this::asMailboxName)
+        Stream<String> appendInMailboxes = 
action.getAppendInMailboxes().getMailboxIds()
+            .stream()
+            .flatMap(this::asMailboxName);
+        Stream<String> moveToMailboxes = action.getMoveTo()
+            .map(moveTo -> validateMailboxName(moveTo.getMailboxName()))
+            .orElse(Stream.empty());
+
+        return Optional.of(Stream.concat(appendInMailboxes, moveToMailboxes)
                 .collect(ImmutableList.toImmutableList()))
             .filter(mailboxes -> !mailboxes.isEmpty());
     }
@@ -222,6 +228,22 @@ public class ActionApplier {
         }
     }
 
+    private Stream<String> validateMailboxName(String mailboxName) {
+        try {
+            MailboxSession mailboxSession = 
mailboxManager.createSystemSession(username);
+            MessageManager messageManager = 
mailboxManager.getMailbox(MailboxPath.forUser(username, mailboxName), 
mailboxSession);
+            mailboxManager.endProcessingRequest(mailboxSession);
+
+            return Stream.of(messageManager.getMailboxPath().getName());
+        } catch (MailboxNotFoundException e) {
+            LOGGER.info("Mailbox {} does not exist for user {}, but it was 
mentioned in a JMAP filtering rule", mailboxName, username.asString(), e);
+            return Stream.empty();
+        } catch (Exception e) {
+            LOGGER.error("Unexpected failure while validating mailbox name {} 
for user {}", mailboxName, username.asString(), e);
+            return Stream.empty();
+        }
+    }
+
     private void sendACopy(LoopPrevention.RecordedRecipients 
recordedRecipients,
                            Set<MailAddress> newRecipients) throws 
MessagingException {
         MailImpl copy = MailImpl.duplicate(mail);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to