Repository: james-project
Updated Branches:
  refs/heads/master 1968a528f -> 072feaca6


JAMES-2529 Mailet Filter implementation


Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/897cb08c
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/897cb08c
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/897cb08c

Branch: refs/heads/master
Commit: 897cb08c54341fd8c3021f0c366197494f39c718
Parents: 1968a52
Author: duc <[email protected]>
Authored: Thu Aug 23 23:09:09 2018 +0700
Committer: Antoine Duprat <[email protected]>
Committed: Thu Aug 30 15:07:00 2018 +0200

----------------------------------------------------------------------
 javax-mail-extension/pom.xml                    |   4 +
 .../org/apache/james/javax/AddressHelper.java   |  37 +
 pom.xml                                         |   5 +
 .../transport/matchers/dlp/DlpDomainRules.java  |  17 +-
 server/protocols/jmap/pom.xml                   |  30 +-
 .../james/jmap/mailet/filter/ActionApplier.java | 101 +++
 .../james/jmap/mailet/filter/JMAPFiltering.java |  85 +++
 .../james/jmap/mailet/filter/MailMatcher.java   | 225 ++++++
 .../james/jmap/mailet/filter/RuleMatcher.java   |  44 ++
 .../mailet/filter/JMAPFilteringExtension.java   | 151 ++++
 .../mailet/filter/JMAPFilteringFixture.java     |  59 ++
 .../jmap/mailet/filter/JMAPFilteringTest.java   | 731 +++++++++++++++++++
 12 files changed, 1471 insertions(+), 18 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/javax-mail-extension/pom.xml
----------------------------------------------------------------------
diff --git a/javax-mail-extension/pom.xml b/javax-mail-extension/pom.xml
index 2e2fa74..d23866d 100644
--- a/javax-mail-extension/pom.xml
+++ b/javax-mail-extension/pom.xml
@@ -40,5 +40,9 @@
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-mime4j-core</artifactId>
+        </dependency>
     </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/javax-mail-extension/src/main/java/org/apache/james/javax/AddressHelper.java
----------------------------------------------------------------------
diff --git 
a/javax-mail-extension/src/main/java/org/apache/james/javax/AddressHelper.java 
b/javax-mail-extension/src/main/java/org/apache/james/javax/AddressHelper.java
new file mode 100644
index 0000000..fb0084c
--- /dev/null
+++ 
b/javax-mail-extension/src/main/java/org/apache/james/javax/AddressHelper.java
@@ -0,0 +1,37 @@
+/****************************************************************
+ * 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.javax;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import javax.mail.Address;
+
+import org.apache.james.mime4j.util.MimeUtil;
+
+public class AddressHelper {
+    public static Stream<String> asStringStream(Address[] addresses) {
+        return Arrays.stream(addresses).map(AddressHelper::asString);
+    }
+
+    private static String asString(Address address) {
+        return MimeUtil.unscrambleHeaderValue(address.toString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 4368273..9c9028a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2401,6 +2401,11 @@
             </dependency>
             <dependency>
                 <groupId>org.junit.platform</groupId>
+                <artifactId>junit-platform-engine</artifactId>
+                <version>${junit.plateform.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.junit.platform</groupId>
                 <artifactId>junit-platform-launcher</artifactId>
                 <version>${junit.plateform.version}</version>
             </dependency>

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/dlp/DlpDomainRules.java
----------------------------------------------------------------------
diff --git 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/dlp/DlpDomainRules.java
 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/dlp/DlpDomainRules.java
index f44a60e..d3f0eef 100644
--- 
a/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/dlp/DlpDomainRules.java
+++ 
b/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/dlp/DlpDomainRules.java
@@ -19,14 +19,14 @@
 
 package org.apache.james.transport.matchers.dlp;
 
+import static org.apache.james.javax.AddressHelper.asStringStream;
+
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
-import javax.mail.Address;
 import javax.mail.BodyPart;
 import javax.mail.Message;
 import javax.mail.MessagingException;
@@ -36,8 +36,8 @@ import javax.mail.internet.MimeMessage;
 import org.apache.james.core.MailAddress;
 import org.apache.james.dlp.api.DLPConfigurationItem;
 import org.apache.james.dlp.api.DLPConfigurationItem.Targets;
+import org.apache.james.javax.AddressHelper;
 import org.apache.james.javax.MultipartUtil;
-import org.apache.james.mime4j.util.MimeUtil;
 import org.apache.james.util.OptionalUtils;
 import org.apache.mailet.Mail;
 
@@ -71,15 +71,6 @@ public class DlpDomainRules {
 
         interface MatcherFunction extends ThrowingPredicate<Mail> { }
 
-
-        private static Stream<String> asStringStream(Address[] addresses) {
-            return Arrays.stream(addresses).map(Rule::asString);
-        }
-
-        private static String asString(Address address) {
-            return MimeUtil.unscrambleHeaderValue(address.toString());
-        }
-
         private static class ContentMatcher implements Rule.MatcherFunction {
 
             private final Pattern pattern;
@@ -153,7 +144,7 @@ public class DlpDomainRules {
             private Stream<String> listHeaderRecipients(Mail mail) throws 
MessagingException {
                 return Optional.ofNullable(mail.getMessage())
                     .flatMap(Throwing.function(m -> 
Optional.ofNullable(m.getAllRecipients())))
-                    .map(Rule::asStringStream)
+                    .map(AddressHelper::asStringStream)
                     .orElse(Stream.of());
             }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/pom.xml
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml
index 8818166..93495fb 100644
--- a/server/protocols/jmap/pom.xml
+++ b/server/protocols/jmap/pom.xml
@@ -78,6 +78,11 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>event-sourcing-event-store-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-mdn</artifactId>
         </dependency>
         <dependency>
@@ -212,11 +217,6 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
             <groupId>net.javacrumbs.json-unit</groupId>
             <artifactId>json-unit-assertj</artifactId>
             <scope>test</scope>
@@ -264,6 +264,26 @@
             <version>1.2</version>
         </dependency>
         <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.vintage</groupId>
+            <artifactId>junit-vintage-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.jgrapht</groupId>
             <artifactId>jgrapht-core</artifactId>
             <version>1.2.0</version>

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
new file mode 100644
index 0000000..fdb55ac
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java
@@ -0,0 +1,101 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.User;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.mailet.Mail;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.annotations.VisibleForTesting;
+
+public class ActionApplier {
+    static final String DELIVERY_PATH_PREFIX = "DeliveryPath_";
+
+    @VisibleForTesting
+    static class Factory {
+        private final MailboxManager mailboxManager;
+        private final MailboxId.Factory mailboxIdFactory;
+
+        @Inject
+        Factory(MailboxManager mailboxManager, MailboxId.Factory 
mailboxIdFactory) {
+            this.mailboxManager = mailboxManager;
+            this.mailboxIdFactory = mailboxIdFactory;
+        }
+
+        public RequireUser forMail(Mail mail) {
+            return new RequireUser(mail);
+        }
+
+        public class RequireUser {
+            private final Mail mail;
+
+            RequireUser(Mail mail) {
+                this.mail = mail;
+            }
+
+            public ActionApplier forUser(User user) {
+                return new ActionApplier(mailboxManager, mailboxIdFactory, 
mail, user);
+            }
+        }
+    }
+
+    private final MailboxManager mailboxManager;
+    private final MailboxId.Factory mailboxIdFactory;
+    private final Mail mail;
+    private final User user;
+
+    @VisibleForTesting
+    public static Factory factory(MailboxManager mailboxManager, 
MailboxId.Factory mailboxIdFactory) {
+        return new Factory(mailboxManager, mailboxIdFactory);
+    }
+
+    private ActionApplier(MailboxManager mailboxManager, MailboxId.Factory 
mailboxIdFactory, Mail mail, User user) {
+        this.mailboxManager = mailboxManager;
+        this.mailboxIdFactory = mailboxIdFactory;
+        this.mail = mail;
+        this.user = user;
+    }
+
+    public void apply(Rule.Action action) {
+        action.getAppendInMailboxes()
+                .getMailboxIds()
+                .stream()
+                .findFirst()
+                .map(mailboxIdFactory::fromString)
+                .ifPresent(Throwing.consumer(this::addStorageDirective));
+    }
+
+    private void addStorageDirective(MailboxId mailboxId) throws 
MailboxException {
+        MailboxSession mailboxSession = 
mailboxManager.createSystemSession(user.asString());
+        MessageManager messageManager = mailboxManager.getMailbox(mailboxId, 
mailboxSession);
+
+        String mailboxName = messageManager.getMailboxPath().getName();
+        String attributeNameForUser = DELIVERY_PATH_PREFIX + user.asString();
+        mail.setAttribute(attributeNameForUser, mailboxName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/JMAPFiltering.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/JMAPFiltering.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/JMAPFiltering.java
new file mode 100644
index 0000000..388fabe
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/JMAPFiltering.java
@@ -0,0 +1,85 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.User;
+import org.apache.james.jmap.api.filtering.FilteringManagement;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.GenericMailet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JMAPFiltering extends GenericMailet {
+
+    private final Logger logger = LoggerFactory.getLogger(JMAPFiltering.class);
+
+    private final FilteringManagement filteringManagement;
+    private final UsersRepository usersRepository;
+    private final ActionApplier.Factory actionApplierFactory;
+
+    @Inject
+    public JMAPFiltering(FilteringManagement filteringManagement,
+                         UsersRepository usersRepository, 
ActionApplier.Factory actionApplierFactory) {
+
+        this.filteringManagement = filteringManagement;
+        this.usersRepository = usersRepository;
+        this.actionApplierFactory = actionApplierFactory;
+    }
+
+    @Override
+    public void service(Mail mail) {
+        mail.getRecipients()
+            .forEach(recipient -> filteringForRecipient(mail, recipient));
+    }
+
+    private void filteringForRecipient(Mail mail, MailAddress recipient) {
+        Optional<User> maybeUser = retrieveUser(recipient);
+        maybeUser
+            .ifPresent(user -> findFirstApplicableRule(user, mail));
+    }
+
+    private void findFirstApplicableRule(User user, Mail mail) {
+        List<Rule> filteringRules = filteringManagement.listRulesForUser(user);
+        RuleMatcher ruleMatcher = new RuleMatcher(filteringRules);
+        Optional<Rule> maybeMatchingRule = 
ruleMatcher.findApplicableRule(mail);
+
+        maybeMatchingRule.ifPresent(rule -> actionApplierFactory.forMail(mail)
+                .forUser(user)
+                .apply(rule.getAction()));
+    }
+
+    private Optional<User> retrieveUser(MailAddress recipient) {
+        try {
+            return 
Optional.ofNullable(User.fromUsername(usersRepository.getUser(recipient)));
+        } catch (UsersRepositoryException e) {
+            logger.error("cannot retrieve user " + recipient.asString(), e);
+            return Optional.empty();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
new file mode 100644
index 0000000..c9ea72b
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
@@ -0,0 +1,225 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+import static org.apache.james.jmap.api.filtering.Rule.Condition;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import javax.mail.Address;
+import javax.mail.Message;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.james.javax.AddressHelper;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.jmap.api.filtering.Rule.Condition.Field;
+import org.apache.mailet.Mail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.functions.ThrowingFunction;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+public interface MailMatcher {
+
+    interface HeaderExtractor extends ThrowingFunction<Mail, Stream<String>> {}
+
+    class HeaderMatcher implements MailMatcher {
+
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(HeaderMatcher.class);
+
+        private final ContentMatcher contentMatcher;
+        private final String ruleValue;
+        private final HeaderExtractor headerExtractor;
+
+        private HeaderMatcher(ContentMatcher contentMatcher, String ruleValue,
+                      HeaderExtractor headerExtractor) {
+            Preconditions.checkNotNull(contentMatcher);
+            Preconditions.checkNotNull(headerExtractor);
+
+            this.contentMatcher = contentMatcher;
+            this.ruleValue = ruleValue;
+            this.headerExtractor = headerExtractor;
+        }
+
+        @Override
+        public boolean match(Mail mail) {
+            try {
+                final Stream<String> headerLines = headerExtractor.apply(mail);
+                return contentMatcher.match(headerLines, ruleValue);
+            } catch (Exception e) {
+                LOGGER.error("error while extracting mail header", e);
+                return false;
+            }
+        }
+    }
+
+    interface ContentMatcher {
+
+        class AddressHeader {
+            private static final Logger LOGGER = 
LoggerFactory.getLogger(AddressHeader.class);
+
+            private final Optional<String> personal;
+            private final Optional<String> address;
+            private final String fullAddress;
+
+            private AddressHeader(String fullAddress) {
+                this.fullAddress = fullAddress;
+                Optional<InternetAddress> internetAddress = parseFullAddress();
+                this.personal = 
internetAddress.map(InternetAddress::getPersonal);
+                this.address = 
internetAddress.map(InternetAddress::getAddress);
+            }
+
+            private Optional<InternetAddress> parseFullAddress() {
+                try {
+                    return Optional.of(new InternetAddress(fullAddress));
+                } catch (AddressException e) {
+                    LOGGER.error("error while parsing full address {}", 
fullAddress, e);
+                    return Optional.empty();
+                }
+            }
+
+            public Optional<String> getPersonal() {
+                return personal;
+            }
+
+            public Optional<String> getAddress() {
+                return address;
+            }
+
+            public String getFullAddress() {
+                return fullAddress;
+            }
+        }
+
+        ContentMatcher STRING_CONTAINS_MATCHER = (contents, valueToMatch) -> 
contents.anyMatch(content -> StringUtils.contains(content, valueToMatch));
+        ContentMatcher STRING_NOT_CONTAINS_MATCHER = 
negate(STRING_CONTAINS_MATCHER);
+        ContentMatcher STRING_EXACTLY_EQUALS_MATCHER = (contents, 
valueToMatch) -> contents.anyMatch(content -> StringUtils.equals(content, 
valueToMatch));
+        ContentMatcher STRING_NOT_EXACTLY_EQUALS_MATCHER = 
negate(STRING_EXACTLY_EQUALS_MATCHER);
+
+        Map<Rule.Condition.Comparator, ContentMatcher> 
CONTENT_STRING_MATCHER_REGISTRY = ImmutableMap.<Rule.Condition.Comparator, 
ContentMatcher>builder()
+                .put(Condition.Comparator.CONTAINS, STRING_CONTAINS_MATCHER)
+                .put(Condition.Comparator.NOT_CONTAINS, 
STRING_NOT_CONTAINS_MATCHER)
+                .put(Condition.Comparator.EXACTLY_EQUALS, 
STRING_EXACTLY_EQUALS_MATCHER)
+                .put(Condition.Comparator.NOT_EXACTLY_EQUALS, 
STRING_NOT_EXACTLY_EQUALS_MATCHER)
+                .build();
+
+        ContentMatcher ADDRESS_CONTAINS_MATCHER = (contents, valueToMatch) -> 
contents
+                .map(ContentMatcher::asAddressHeader)
+                .anyMatch(addressHeader -> 
StringUtils.containsIgnoreCase(addressHeader.getFullAddress(), valueToMatch));
+
+        ContentMatcher ADDRESS_NOT_CONTAINS_MATCHER = 
negate(ADDRESS_CONTAINS_MATCHER);
+        ContentMatcher ADDRESS_EXACTLY_EQUALS_MATCHER = (contents, 
valueToMatch) -> contents
+                .map(ContentMatcher::asAddressHeader)
+                .anyMatch(addressHeader ->
+                        
StringUtils.equalsIgnoreCase(addressHeader.getFullAddress(), valueToMatch)
+                        || 
StringUtils.equalsIgnoreCase(addressHeader.getAddress().orElse(null), 
valueToMatch)
+                        || 
StringUtils.equalsIgnoreCase(addressHeader.getPersonal().orElse(null), 
valueToMatch));
+
+        ContentMatcher ADDRESS_NOT_EXACTLY_EQUALS_MATCHER = 
negate(ADDRESS_EXACTLY_EQUALS_MATCHER);
+
+        Map<Rule.Condition.Comparator, ContentMatcher> 
HEADER_ADDRESS_MATCHER_REGISTRY = ImmutableMap.<Rule.Condition.Comparator, 
ContentMatcher>builder()
+                .put(Condition.Comparator.CONTAINS, ADDRESS_CONTAINS_MATCHER)
+                .put(Condition.Comparator.NOT_CONTAINS, 
ADDRESS_NOT_CONTAINS_MATCHER)
+                .put(Condition.Comparator.EXACTLY_EQUALS, 
ADDRESS_EXACTLY_EQUALS_MATCHER)
+                .put(Condition.Comparator.NOT_EXACTLY_EQUALS, 
ADDRESS_NOT_EXACTLY_EQUALS_MATCHER)
+                .build();
+
+        Map<Rule.Condition.Field, Map<Rule.Condition.Comparator, 
ContentMatcher>> CONTENT_MATCHER_REGISTRY = ImmutableMap.<Rule.Condition.Field, 
Map<Rule.Condition.Comparator, ContentMatcher>>builder()
+                .put(Condition.Field.SUBJECT, CONTENT_STRING_MATCHER_REGISTRY)
+                .put(Condition.Field.TO, HEADER_ADDRESS_MATCHER_REGISTRY)
+                .put(Condition.Field.CC, HEADER_ADDRESS_MATCHER_REGISTRY)
+                .put(Condition.Field.RECIPIENT, 
HEADER_ADDRESS_MATCHER_REGISTRY)
+                .put(Condition.Field.FROM, HEADER_ADDRESS_MATCHER_REGISTRY)
+                .build();
+
+        static ContentMatcher negate(ContentMatcher contentMatcher) {
+            return (Stream<String> contents, String valueToMatch) ->
+                    !contentMatcher.match(contents, valueToMatch);
+        }
+
+        static Optional<ContentMatcher> asContentMatcher(Condition.Field 
field, Condition.Comparator comparator) {
+            return Optional
+                .ofNullable(CONTENT_MATCHER_REGISTRY.get(field))
+                .map(matcherRegistry -> matcherRegistry.get(comparator));
+        }
+
+        static AddressHeader asAddressHeader(String addressAsString) {
+            return new AddressHeader(addressAsString);
+        }
+
+        boolean match(Stream<String> contents, String valueToMatch);
+    }
+
+    HeaderExtractor SUBJECT_EXTRACTOR = mail ->
+            Optional.ofNullable(mail.getMessage().getSubject())
+                    .map(Stream::of)
+                    .orElse(Stream.empty());
+    HeaderExtractor RECIPIENT_EXTRACTOR =  mail -> addressExtractor(
+            ArrayUtils.addAll(
+                mail.getMessage().getRecipients(Message.RecipientType.TO),
+                mail.getMessage().getRecipients(Message.RecipientType.CC)));
+
+    HeaderExtractor FROM_EXTRACTOR = mail -> 
addressExtractor(mail.getMessage().getFrom());
+    HeaderExtractor CC_EXTRACTOR = 
recipientExtractor(Message.RecipientType.CC);
+    HeaderExtractor TO_EXTRACTOR = 
recipientExtractor(Message.RecipientType.TO);
+
+    Map<Field, HeaderExtractor> HEADER_EXTRACTOR_REGISTRY = 
ImmutableMap.<Field, HeaderExtractor>builder()
+            .put(Field.SUBJECT, SUBJECT_EXTRACTOR)
+            .put(Field.RECIPIENT, RECIPIENT_EXTRACTOR)
+            .put(Field.FROM, FROM_EXTRACTOR)
+            .put(Field.CC, CC_EXTRACTOR)
+            .put(Field.TO, TO_EXTRACTOR)
+            .build();
+
+    static MailMatcher from(Rule rule) {
+        Condition ruleCondition = rule.getCondition();
+        Optional<ContentMatcher> maybeContentMatcher = 
ContentMatcher.asContentMatcher(ruleCondition.getField(), 
ruleCondition.getComparator());
+        Optional<HeaderExtractor> maybeHeaderExtractor = 
getHeaderExtractor(ruleCondition.getField());
+
+        return new HeaderMatcher(
+            maybeContentMatcher.orElseThrow(() -> new RuntimeException("No 
content matcher associated with field " + ruleCondition.getField())),
+            rule.getCondition().getValue(),
+            maybeHeaderExtractor.orElseThrow(() -> new RuntimeException("No 
content matcher associated with comparator " + ruleCondition.getComparator())));
+    }
+
+    static HeaderExtractor recipientExtractor(Message.RecipientType type) {
+        return mail -> addressExtractor(mail.getMessage().getRecipients(type));
+    }
+
+    static Stream<String> addressExtractor(Address[] addresses) {
+        return Optional.ofNullable(addresses)
+                .map(AddressHelper::asStringStream)
+                .orElse(Stream.empty());
+    }
+
+    static Optional<HeaderExtractor> getHeaderExtractor(Field field) {
+        return Optional
+            .ofNullable(HEADER_EXTRACTOR_REGISTRY.get(field));
+    }
+
+    boolean match(Mail mail);
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
new file mode 100644
index 0000000..abc1da1
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
@@ -0,0 +1,44 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.mailet.Mail;
+
+import com.google.common.base.Preconditions;
+
+class RuleMatcher {
+    private final List<Rule> filteringRules;
+
+    RuleMatcher(List<Rule> filteringRules) {
+        Preconditions.checkNotNull(filteringRules);
+
+        this.filteringRules = filteringRules;
+    }
+
+    Optional<Rule> findApplicableRule(Mail mail) {
+        return filteringRules.stream()
+            .filter(rule -> MailMatcher.from(rule).match(mail))
+            .findFirst();
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
new file mode 100644
index 0000000..05dc317
--- /dev/null
+++ 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.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.jmap.mailet.filter;
+
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1_MAILBOX_1;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1_USERNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_ADDRESS;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.mail.MessagingException;
+
+import org.apache.james.core.User;
+import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.eventsourcing.eventstore.memory.InMemoryEventStore;
+import org.apache.james.jmap.api.filtering.FilteringManagement;
+import org.apache.james.jmap.api.filtering.Rule;
+import 
org.apache.james.jmap.api.filtering.impl.EventSourcingFilteringManagement;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.acl.SimpleGroupMembershipResolver;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.mailet.base.test.FakeMail;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+
+import com.google.common.collect.ImmutableList;
+
+public class JMAPFilteringExtension implements BeforeEachCallback, 
ParameterResolver {
+
+    class JMAPFilteringTestSystem {
+
+        private final JMAPFiltering jmapFiltering;
+        private final FilteringManagement filteringManagement;
+        private final InMemoryMailboxManager mailboxManager;
+        private final MailboxId recipient1Mailbox;
+
+        JMAPFilteringTestSystem(JMAPFiltering jmapFiltering, 
FilteringManagement filteringManagement,
+                                InMemoryMailboxManager mailboxManager) {
+            this.jmapFiltering = jmapFiltering;
+            this.filteringManagement = filteringManagement;
+            this.mailboxManager = mailboxManager;
+            try {
+                this.recipient1Mailbox = createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, RECIPIENT_1_MAILBOX_1);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public JMAPFiltering getJmapFiltering() {
+            return jmapFiltering;
+        }
+
+        public FilteringManagement getFilteringManagement() {
+            return filteringManagement;
+        }
+
+        public InMemoryMailboxManager getMailboxManager() {
+            return mailboxManager;
+        }
+
+        public MailboxId getRecipient1MailboxId() {
+            return recipient1Mailbox;
+        }
+
+        public MailboxId createMailbox(InMemoryMailboxManager mailboxManager, 
String username, String mailboxName) throws Exception {
+            MailboxSession mailboxSession = 
mailboxManager.createSystemSession(username);
+            return mailboxManager
+                .createMailbox(MailboxPath.forUser(username, mailboxName), 
mailboxSession)
+                .orElseThrow(() -> new RuntimeException("Missing mailboxId 
when creating mailbox"));
+        }
+
+        public void defineRulesForRecipient1(Rule.Condition... conditions) {
+            defineRulesForRecipient1(Arrays.asList(conditions));
+        }
+
+        public void defineRulesForRecipient1(List<Rule.Condition> conditions) {
+            AtomicInteger counter = new AtomicInteger();
+            ImmutableList<Rule> rules = conditions
+                .stream()
+                .map(condition -> Rule.builder()
+                    .id(Rule.Id.of(String.valueOf(counter.incrementAndGet())))
+                    .name(String.valueOf(counter.incrementAndGet()))
+                    .condition(condition)
+                    
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
+                    .build())
+                .collect(ImmutableList.toImmutableList());
+
+            
testSystem.getFilteringManagement().defineRulesForUser(User.fromUsername(RECIPIENT_1_USERNAME),
 rules);
+        }
+
+        public FakeMail asMail(MimeMessageBuilder mimeMessageBuilder) throws 
MessagingException {
+            return FakeMail.builder()
+                .sender(USER_1_ADDRESS)
+                .recipients(RECIPIENT_1)
+                .mimeMessage(mimeMessageBuilder)
+                .build();
+        }
+    }
+
+    private JMAPFilteringTestSystem testSystem;
+
+    @Override
+    public void beforeEach(ExtensionContext extensionContext) throws Exception 
{
+        FilteringManagement filteringManagement = new 
EventSourcingFilteringManagement(new InMemoryEventStore());
+        MemoryUsersRepository usersRepository = 
MemoryUsersRepository.withoutVirtualHosting();
+        InMemoryMailboxManager mailboxManager = new 
InMemoryIntegrationResources().createMailboxManager(new 
SimpleGroupMembershipResolver());
+        ActionApplier.Factory actionApplierFactory = 
ActionApplier.factory(mailboxManager, new InMemoryId.Factory());
+
+        JMAPFiltering jmapFiltering = new JMAPFiltering(filteringManagement, 
usersRepository, actionApplierFactory);
+
+        testSystem = new JMAPFilteringTestSystem(jmapFiltering, 
filteringManagement, mailboxManager);
+    }
+
+    @Override
+    public boolean supportsParameter(ParameterContext parameterContext, 
ExtensionContext extensionContext) throws ParameterResolutionException {
+        return (parameterContext.getParameter().getType() == 
JMAPFilteringTestSystem.class);
+    }
+
+    @Override
+    public Object resolveParameter(ParameterContext parameterContext, 
ExtensionContext extensionContext) throws ParameterResolutionException {
+        return testSystem;
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringFixture.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringFixture.java
 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringFixture.java
new file mode 100644
index 0000000..1b4792b
--- /dev/null
+++ 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringFixture.java
@@ -0,0 +1,59 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+public interface JMAPFilteringFixture {
+    String GA_BOU_ZO_MEU_FULL_ADDRESS = "GA BOU ZO MEU 
<[email protected]>";
+    String BOU = "BOU";
+
+    String USER_1_FULL_ADDRESS = "user1 <[email protected]>";
+    String USER_1_ADDRESS = "[email protected]";
+    String USER_1_USERNAME = "user1";
+
+    String USER_2_FULL_ADDRESS = "user2 <[email protected]>";
+    String USER_2_ADDRESS = "[email protected]";
+    String USER_2_USERNAME = "user2";
+
+    String USER_1_AND_UNFOLDED_USER_FULL_ADDRESS = "user2 <[email protected]>, 
\r\nunfolded\r\n_user\r\n <[email protected]>";
+
+    String USER_3_FULL_ADDRESS = "user3 <[email protected]>";
+    String USER_3_ADDRESS = "[email protected]";
+    String USER_3_USERNAME = "user3";
+
+    String USER_4_FULL_ADDRESS = "user4 <[email protected]>";
+
+    String SCRAMBLED_SUBJECT = "this is the subject 
=?UTF-8?B?RnLDqWTDqXJpYyBNQVJUSU4=?= of the mail";
+    String UNSCRAMBLED_SUBJECT = "this is the subject Frédéric MARTIN of the 
mail";
+    String SHOULD_NOT_MATCH = "should not match";
+
+    String RECIPIENT_1 = "[email protected]";
+    String RECIPIENT_1_USERNAME = "recipient1";
+    String RECIPIENT_1_MAILBOX_1 = "recipient1_maibox1";
+
+    String FRED_MARTIN_FULLNAME = "Frédéric MARTIN";
+    String FRED_MARTIN_FULL_SCRAMBLED_ADDRESS = 
"=?UTF-8?B?RnLDqWTDqXJpYyBNQVJUSU4=?= <[email protected]>";
+
+    String UNFOLDED_USERNAME = "unfolded_user";
+
+    String EMPTY = "";
+
+    String TO_HEADER = "to";
+    String CC_HEADER = "cc";
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/897cb08c/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
new file mode 100644
index 0000000..0d697f8
--- /dev/null
+++ 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
@@ -0,0 +1,731 @@
+/****************************************************************
+ * 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.jmap.mailet.filter;
+
+import static 
org.apache.james.core.builder.MimeMessageBuilder.mimeMessageBuilder;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.CONTAINS;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.EXACTLY_EQUALS;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.NOT_CONTAINS;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.NOT_EXACTLY_EQUALS;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Field.CC;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Field.FROM;
+import static 
org.apache.james.jmap.api.filtering.Rule.Condition.Field.RECIPIENT;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Field.SUBJECT;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Field.TO;
+import static 
org.apache.james.jmap.mailet.filter.ActionApplier.DELIVERY_PATH_PREFIX;
+import static org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.BOU;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.CC_HEADER;
+import static org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.EMPTY;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.FRED_MARTIN_FULLNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.FRED_MARTIN_FULL_SCRAMBLED_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.GA_BOU_ZO_MEU_FULL_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1_MAILBOX_1;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.RECIPIENT_1_USERNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.SCRAMBLED_SUBJECT;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.SHOULD_NOT_MATCH;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.TO_HEADER;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.UNFOLDED_USERNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.UNSCRAMBLED_SUBJECT;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_AND_UNFOLDED_USER_FULL_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_FULL_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_USERNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_2_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_2_FULL_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_3_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_3_FULL_ADDRESS;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_3_USERNAME;
+import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_4_FULL_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.james.core.User;
+import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.jmap.api.filtering.Rule;
+import 
org.apache.james.jmap.mailet.filter.JMAPFilteringExtension.JMAPFilteringTestSystem;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.util.StreamUtils;
+import org.apache.mailet.base.test.FakeMail;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+@ExtendWith(JMAPFilteringExtension.class)
+class JMAPFilteringTest {
+
+    static class FilteringArgumentBuilder {
+        private Optional<String> description;
+        private Optional<Rule.Condition.Field> field;
+        private MimeMessageBuilder mimeMessageBuilder;
+        private Optional<String> valueToMatch;
+        
+        private FilteringArgumentBuilder() {
+            this.description = Optional.empty();
+            this.field = Optional.empty();
+            mimeMessageBuilder = MimeMessageBuilder.mimeMessageBuilder();
+            this.valueToMatch = Optional.empty();
+        }
+
+        public FilteringArgumentBuilder description(String description) {
+            this.description = Optional.ofNullable(description);
+            return this;
+        }
+
+        public FilteringArgumentBuilder field(Rule.Condition.Field field) {
+            this.field = Optional.ofNullable(field);
+            return this;
+        }
+
+        public FilteringArgumentBuilder from(String from) {
+            
Optional.ofNullable(from).ifPresent(Throwing.consumer(mimeMessageBuilder::addFrom));
+            return this;
+        }
+
+        public FilteringArgumentBuilder noHeader() {
+            return this;
+        }
+
+        public FilteringArgumentBuilder toRecipient(String toRecipient) {
+            
Optional.ofNullable(toRecipient).ifPresent(Throwing.consumer(mimeMessageBuilder::addToRecipient));
+            return this;
+        }
+
+        public FilteringArgumentBuilder ccRecipient(String ccRecipient) {
+            
Optional.ofNullable(ccRecipient).ifPresent(Throwing.consumer(mimeMessageBuilder::addCcRecipient));
+            return this;
+        }
+
+        public FilteringArgumentBuilder bccRecipient(String bccRecipient) {
+            
Optional.ofNullable(bccRecipient).ifPresent(Throwing.consumer(mimeMessageBuilder::addBccRecipient));
+            return this;
+        }
+
+        public FilteringArgumentBuilder header(String headerName, String 
headerValue) {
+            mimeMessageBuilder.addHeader(headerName, headerValue);
+            return this;
+        }
+
+        public FilteringArgumentBuilder headerForField(String headerValue) {
+            Preconditions.checkState(field.isPresent(), "field should be set 
first");
+
+            mimeMessageBuilder.addHeader(field.get().asString(), headerValue);
+            return this;
+        }
+
+        public FilteringArgumentBuilder subject(String subject) {
+            mimeMessageBuilder.setSubject(subject);
+            return this;
+        }
+
+        public FilteringArgumentBuilder valueToMatch(String valueToMatch) {
+            this.valueToMatch = Optional.ofNullable(valueToMatch);
+            return this;
+        }
+
+        public FilteringArgumentBuilder scrambledSubjectToMatch(String 
valueToMatch) {
+            return description("normal content")
+                .field(SUBJECT)
+                .subject(SCRAMBLED_SUBJECT)
+                .valueToMatch(valueToMatch);
+        }
+
+        public FilteringArgumentBuilder 
scrambledSubjectShouldNotMatchCaseSensitive() {
+            return description("normal content (case sensitive)")
+                .field(SUBJECT)
+                .subject(SCRAMBLED_SUBJECT)
+                .valueToMatch(SCRAMBLED_SUBJECT.toUpperCase(Locale.FRENCH));
+        }
+
+        public FilteringArgumentBuilder unscrambledSubjectToMatch(String 
valueToMatch) {
+            return description("unscrambled content")
+                .field(SUBJECT)
+                .subject(UNSCRAMBLED_SUBJECT)
+                .valueToMatch(valueToMatch);
+        }
+
+        public FilteringArgumentBuilder 
unscrambledSubjectShouldNotMatchCaseSensitive() {
+            return description("unscrambled content (case sensitive)")
+                    .field(SUBJECT)
+                    .subject(UNSCRAMBLED_SUBJECT)
+                    
.valueToMatch(UNSCRAMBLED_SUBJECT.toUpperCase(Locale.FRENCH));
+        }
+
+        public Arguments build() {
+            Preconditions.checkState(description.isPresent());
+            Preconditions.checkState(field.isPresent());
+            Preconditions.checkState(valueToMatch.isPresent());
+            
+            return Arguments.of(description.get(), field.get(), 
mimeMessageBuilder, valueToMatch.get());
+        }
+
+    }
+
+    static FilteringArgumentBuilder argumentBuilder() {
+        return new FilteringArgumentBuilder();
+    }
+
+    static FilteringArgumentBuilder argumentBuilder(Rule.Condition.Field 
field) {
+        return new FilteringArgumentBuilder()
+            .field(field);
+    }
+
+    static Stream<Arguments> exactlyEqualsTestSuite() {
+        return StreamUtils.flatten(
+            Stream.of(FROM, TO, CC)
+                .flatMap(headerField -> Stream.of(
+                        argumentBuilder(headerField)
+                        .description("full address value")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch(USER_1_USERNAME),
+                    argumentBuilder(headerField)
+                        .description("full address value (different case)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        
.valueToMatch(USER_1_USERNAME.toUpperCase(Locale.ENGLISH)),
+                    argumentBuilder(headerField)
+                        .description("address only value")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch(USER_1_ADDRESS),
+                    argumentBuilder(headerField)
+                        .description("address only value (different case)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        
.valueToMatch(USER_1_ADDRESS.toUpperCase(Locale.ENGLISH)),
+                    argumentBuilder(headerField)
+                        .description("personal only value")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch(USER_1_FULL_ADDRESS),
+                    argumentBuilder(headerField)
+                        .description("personal only value (different case)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch(USER_1_FULL_ADDRESS.toUpperCase()),
+                    argumentBuilder(headerField)
+                        .description("personal header should match personal")
+                        .headerForField(USER_1_USERNAME)
+                        .valueToMatch(USER_1_USERNAME),
+                    argumentBuilder(headerField)
+                        .description("address header should match address")
+                        .headerForField(USER_1_ADDRESS)
+                        .valueToMatch(USER_1_ADDRESS),
+                    argumentBuilder(headerField)
+                        .description("multiple headers")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .headerForField(USER_2_FULL_ADDRESS)
+                        .valueToMatch(USER_1_USERNAME),
+                    argumentBuilder(headerField)
+                        .description("scrambled content")
+                        .headerForField(FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch(FRED_MARTIN_FULLNAME),
+                    argumentBuilder(headerField)
+                        .description("folded content")
+                        .headerForField(USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(UNFOLDED_USERNAME),
+                    argumentBuilder(headerField)
+                        .description("folded content (different case)")
+                        .headerForField(USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(UNFOLDED_USERNAME.toUpperCase())
+                    ).map(FilteringArgumentBuilder::build)),
+            Stream.of(TO_HEADER, CC_HEADER)
+                .flatMap(headerName -> Stream.of(
+                    argumentBuilder()
+                        .description("full address " + headerName + " header")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch(USER_3_FULL_ADDRESS)
+                        .build(),
+                    argumentBuilder()
+                        .description("full address " + headerName + " header 
(different case)")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        
.valueToMatch(USER_3_FULL_ADDRESS.toUpperCase(Locale.ENGLISH))
+                        .build(),
+                    argumentBuilder()
+                        .description("address only " + headerName + " header")
+                        .field(RECIPIENT).header(headerName, 
USER_3_FULL_ADDRESS)
+                        .valueToMatch(USER_3_ADDRESS)
+                        .build(),
+                    argumentBuilder()
+                        .description("personal only " + headerName + " header")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch(USER_3_USERNAME)
+                        .build(),
+                    argumentBuilder()
+                        .description("scrambled content in " + headerName + " 
header")
+                        .field(RECIPIENT)
+                        .header(headerName, FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch(FRED_MARTIN_FULLNAME)
+                        .build(),
+                    argumentBuilder()
+                        .description("folded content in " + headerName + " 
header")
+                        .field(RECIPIENT)
+                        .header(headerName, 
USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(UNFOLDED_USERNAME)
+                        .build())),
+            Stream.of(
+                argumentBuilder().description("multiple to and cc 
headers").field(RECIPIENT)
+                    .ccRecipient(USER_1_FULL_ADDRESS)
+                    .ccRecipient(USER_2_FULL_ADDRESS)
+                    .toRecipient(USER_3_FULL_ADDRESS)
+                    .toRecipient(USER_4_FULL_ADDRESS)
+                    .valueToMatch(USER_4_FULL_ADDRESS)
+                    .build(),
+                
argumentBuilder().scrambledSubjectToMatch(UNSCRAMBLED_SUBJECT).build(),
+                
argumentBuilder().unscrambledSubjectToMatch(UNSCRAMBLED_SUBJECT).build()));
+    }
+
+    static Stream<Arguments> containsTestSuite() {
+        return Stream.concat(
+            exactlyEqualsTestSuite(),
+            containsArguments());
+    }
+
+    private static Stream<Arguments> containsArguments() {
+        return StreamUtils.flatten(
+            Stream.of(FROM, TO, CC)
+                .flatMap(headerField -> Stream.of(
+                    argumentBuilder(headerField)
+                        .description("full address value (partial matching)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch("ser1 <"),
+                    argumentBuilder(headerField)
+                        .description("full address value (partial matching, 
different case)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch("SER1 <"),
+                    argumentBuilder(headerField)
+                        .description("address only value (partial matching)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch("ser1@jam"),
+                    argumentBuilder(headerField)
+                        .description("personal only value (partial matching)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch("ser1"),
+                    argumentBuilder(headerField)
+                        .description("address header & match in the address 
(partial matching)")
+                        .headerForField(USER_1_ADDRESS)
+                        .valueToMatch("ser1@jam"),
+                    argumentBuilder(headerField)
+                        .description("raw value matching (partial matching)")
+                        .headerForField(GA_BOU_ZO_MEU_FULL_ADDRESS)
+                        .valueToMatch(BOU),
+                    argumentBuilder(headerField)
+                        .description("multiple headers (partial matching)")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .headerForField(USER_2_FULL_ADDRESS)
+                        .valueToMatch("ser1@jam"),
+                    argumentBuilder(headerField)
+                        .description("scrambled content (partial matching)")
+                        .headerForField(FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch("déric MAR"),
+                    argumentBuilder(headerField)
+                        .description("folded content (partial matching)")
+                        .headerForField(USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch("ded_us"))
+                    .map(FilteringArgumentBuilder::build)),
+            Stream.of(TO_HEADER, CC_HEADER)
+                .flatMap(headerName -> Stream.of(
+                    argumentBuilder()
+                        .description("full address " + headerName + " header 
(partial matching)")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch("ser3 <us")
+                        .build(),
+                    argumentBuilder()
+                        .description("full address " + headerName + " header 
(partial matching, different case)")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch("SER3 <US")
+                        .build(),
+                    argumentBuilder()
+                        .description("address only " + headerName + " header 
(partial matching)")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch("ser3@jam")
+                        .build(),
+                    argumentBuilder()
+                        .description("personal only " + headerName + " header 
(partial matching)")
+                        .field(RECIPIENT)
+                        .header(headerName, USER_3_FULL_ADDRESS)
+                        .valueToMatch("ser3")
+                        .build(),
+                    argumentBuilder()
+                        .description("scrambled content in " + headerName + " 
header (partial matching)")
+                        .field(RECIPIENT)
+                        .header(headerName, FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch("déric MAR")
+                        .build(),
+                    argumentBuilder()
+                        .description("folded content in " + headerName + " 
header (partial matching)")
+                        .field(RECIPIENT)
+                        .header(headerName, 
USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch("folded_us")
+                        .build())),
+            Stream.of(
+                argumentBuilder().description("multiple to and cc headers 
(partial matching)").field(RECIPIENT)
+                    .ccRecipient(USER_1_FULL_ADDRESS)
+                    .ccRecipient(USER_2_FULL_ADDRESS)
+                    .toRecipient(USER_3_FULL_ADDRESS)
+                    .toRecipient(USER_4_FULL_ADDRESS)
+                    .valueToMatch("user4@jam").build(),
+                argumentBuilder().scrambledSubjectToMatch("is the 
subject").build(),
+                argumentBuilder().unscrambledSubjectToMatch("rédéric 
MART").build()));
+    }
+
+    static Stream<Arguments> notEqualsTestSuite() {
+        return Stream.concat(
+            notContainsTestSuite(),
+            containsArguments());
+    }
+
+    static Stream<Arguments> notContainsTestSuite() {
+        return StreamUtils.flatten(
+            Stream.of(FROM, TO, CC)
+                .flatMap(headerField -> Stream.of(
+                    argumentBuilder(headerField)
+                        .description("normal content")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH),
+                    argumentBuilder(headerField)
+                        .description("multiple headers")
+                        .headerForField(USER_1_FULL_ADDRESS)
+                        .from(USER_2_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH),
+                    argumentBuilder(headerField)
+                        .description("scrambled content")
+                        .headerForField(FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH),
+                    argumentBuilder(headerField)
+                        .description("folded content")
+                        .headerForField(USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH),
+                    argumentBuilder(headerField)
+                        .description("empty content")
+                        .headerForField(EMPTY)
+                        .valueToMatch(SHOULD_NOT_MATCH))
+                    .map(FilteringArgumentBuilder::build)),
+            Stream.of(TO_HEADER, CC_HEADER)
+                .flatMap(headerName -> Stream.of(
+                    argumentBuilder()
+                        .description("normal content " + headerName + " 
header")
+                        .field(RECIPIENT).header(headerName, 
USER_3_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH)
+                        .build(),
+                    argumentBuilder()
+                        .description("scrambled content in " + headerName + " 
header")
+                        .field(RECIPIENT).header(headerName, 
FRED_MARTIN_FULL_SCRAMBLED_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH)
+                        .build(),
+                    argumentBuilder()
+                        .description("folded content in " + headerName + " 
header")
+                        .field(RECIPIENT)
+                        .header(headerName, 
USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH)
+                        .build(),
+                    argumentBuilder()
+                        .description("bcc header")
+                        .field(RECIPIENT)
+                        .header(headerName, 
USER_1_AND_UNFOLDED_USER_FULL_ADDRESS)
+                        .valueToMatch(SHOULD_NOT_MATCH)
+                        .build())),
+            Stream.of(
+                argumentBuilder().description("multiple to and cc 
headers").field(RECIPIENT)
+                    .ccRecipient(USER_1_FULL_ADDRESS)
+                    .ccRecipient(USER_2_FULL_ADDRESS)
+                    .toRecipient(USER_3_FULL_ADDRESS)
+                    .toRecipient(USER_4_FULL_ADDRESS)
+                    .valueToMatch(SHOULD_NOT_MATCH)
+                    .build(),
+                argumentBuilder().description("matching bcc 
headers").field(RECIPIENT)
+                    .bccRecipient(USER_1_FULL_ADDRESS)
+                    .valueToMatch(USER_1_FULL_ADDRESS)
+                    .build(),
+                
argumentBuilder().scrambledSubjectToMatch(SHOULD_NOT_MATCH).build(),
+                
argumentBuilder().scrambledSubjectShouldNotMatchCaseSensitive().build(),
+                
argumentBuilder().unscrambledSubjectToMatch(SHOULD_NOT_MATCH).build(),
+                
argumentBuilder().unscrambledSubjectShouldNotMatchCaseSensitive().build()),
+            Stream.of(Rule.Condition.Field.values())
+                .map(field -> argumentBuilder()
+                    .description("no header")
+                    .field(field)
+                    .noHeader()
+                    .valueToMatch(USER_1_USERNAME)
+                    .build()));
+    }
+
+    @ParameterizedTest(name = "CONTAINS should match for header field {1}, 
with {0}")
+    @MethodSource("containsTestSuite")
+    void matchingContainsTest(String testDescription,
+                              Rule.Condition.Field fieldToMatch,
+                              MimeMessageBuilder mimeMessageBuilder,
+                              String valueToMatch,
+                              JMAPFilteringTestSystem testSystem) throws 
Exception {
+
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
CONTAINS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isEqualTo(RECIPIENT_1_MAILBOX_1);
+    }
+
+    @ParameterizedTest(name = "CONTAINS should not match for header field {1}, 
with {0}")
+    @MethodSource("notContainsTestSuite")
+    void notMatchingContainsTest(String testDescription,
+                              Rule.Condition.Field fieldToMatch,
+                              MimeMessageBuilder mimeMessageBuilder,
+                              String valueToMatch,
+                              JMAPFilteringTestSystem testSystem) throws 
Exception {
+
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
CONTAINS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isNull();
+    }
+
+    @ParameterizedTest(name = "NOT-CONTAINS should be matching for field {1}, 
with {0}")
+    @MethodSource("notContainsTestSuite")
+    void matchingNotContainsTest(String testDescription,
+                                 Rule.Condition.Field fieldToMatch,
+                                 MimeMessageBuilder mimeMessageBuilder,
+                                 String valueToMatch,
+                                 JMAPFilteringTestSystem testSystem) throws 
Exception {
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
NOT_CONTAINS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isEqualTo(RECIPIENT_1_MAILBOX_1);
+    }
+
+
+    @ParameterizedTest(name = "NOT-CONTAINS should not be matching for field 
{1}, with {0}")
+    @MethodSource("containsTestSuite")
+    void notContainsNotMatchingTest(String testDescription,
+                                    Rule.Condition.Field fieldToMatch,
+                                    MimeMessageBuilder mimeMessageBuilder,
+                                    String valueToMatch,
+                                    JMAPFilteringTestSystem testSystem) throws 
Exception {
+
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
NOT_CONTAINS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isNull();
+    }
+
+    @ParameterizedTest(name = "EXACTLY-EQUALS should match for header field 
{1}, with {0}")
+    @MethodSource("exactlyEqualsTestSuite")
+    void equalsMatchingTest(String testDescription,
+                            Rule.Condition.Field fieldToMatch,
+                            MimeMessageBuilder mimeMessageBuilder,
+                            String valueToMatch,
+                            JMAPFilteringTestSystem testSystem) throws 
Exception {
+
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
EXACTLY_EQUALS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isEqualTo(RECIPIENT_1_MAILBOX_1);
+    }
+
+    @ParameterizedTest(name = "EXACTLY-EQUALS should not match for header 
field {1}, with {0}")
+    @MethodSource("notEqualsTestSuite")
+    void equalsNotMatchingTest(String testDescription,
+                            Rule.Condition.Field fieldToMatch,
+                            MimeMessageBuilder mimeMessageBuilder,
+                            String valueToMatch,
+                            JMAPFilteringTestSystem testSystem) throws 
Exception {
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
EXACTLY_EQUALS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isNull();
+    }
+
+    @ParameterizedTest(name = "NOT_EXACTLY_EQUALS should match for header 
field {1}, with {0}")
+    @MethodSource("notEqualsTestSuite")
+    void notEqualsMatchingTest(String testDescription,
+                               Rule.Condition.Field fieldToMatch,
+                               MimeMessageBuilder mimeMessageBuilder,
+                               String valueToMatch,
+                               JMAPFilteringTestSystem testSystem) throws 
Exception {
+
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
NOT_EXACTLY_EQUALS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isEqualTo(RECIPIENT_1_MAILBOX_1);
+    }
+
+    @ParameterizedTest(name = "NOT_EXACTLY_EQUALS should not match for header 
field {1}, with {0}")
+    @MethodSource("exactlyEqualsTestSuite")
+    void notMatchingNotEqualsTests(String testDescription,
+                                   Rule.Condition.Field fieldToMatch,
+                                   MimeMessageBuilder mimeMessageBuilder,
+                                   String valueToMatch,
+                                   JMAPFilteringTestSystem testSystem) throws 
Exception {
+        testSystem.defineRulesForRecipient1(Rule.Condition.of(fieldToMatch, 
NOT_EXACTLY_EQUALS, valueToMatch));
+        FakeMail mail = testSystem.asMail(mimeMessageBuilder);
+        testSystem.getJmapFiltering().service(mail);
+
+        assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+            .isNull();
+    }
+
+    @Nested
+    class MultiRuleBehaviourTest {
+        @Test
+        void 
mailDirectiveShouldSetFirstMatchedRuleWhenMultipleRules(JMAPFilteringTestSystem 
testSystem) throws Exception {
+            InMemoryMailboxManager mailboxManager = 
testSystem.getMailboxManager();
+            MailboxId mailbox1Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_1");
+            MailboxId mailbox2Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_2");
+            MailboxId mailbox3Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_3");
+
+            testSystem.defineRulesForRecipient1();
+            
testSystem.getFilteringManagement().defineRulesForUser(User.fromUsername(RECIPIENT_1_USERNAME),
+                ImmutableList.of(
+                    Rule.builder()
+                        .id(Rule.Id.of("1"))
+                        .name("rule 1")
+                        .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(mailbox1Id.serialize())))
+                        .build(),
+                    Rule.builder()
+                        .id(Rule.Id.of("2"))
+                        .name("rule 2")
+                        .condition(Rule.Condition.of(FROM, NOT_CONTAINS, 
USER_1_USERNAME))
+                        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(mailbox2Id.serialize())))
+                        .build(),
+                    Rule.builder()
+                        .id(Rule.Id.of("3"))
+                        .name("rule 3")
+                        .condition(Rule.Condition.of(TO, EXACTLY_EQUALS, 
USER_3_ADDRESS))
+                        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(mailbox3Id.serialize())))
+                        .build()));
+
+            FakeMail mail = FakeMail.builder()
+                .sender(USER_1_ADDRESS)
+                .recipients(RECIPIENT_1)
+                .mimeMessage(mimeMessageBuilder()
+                    .addFrom(USER_2_ADDRESS)
+                    .addToRecipient(USER_3_ADDRESS)
+                    .setSubject(UNSCRAMBLED_SUBJECT))
+                .build();
+
+            testSystem.getJmapFiltering().service(mail);
+
+            assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isEqualTo("RECIPIENT_1_MAILBOX_1");
+        }
+
+        @Test
+        void 
mailDirectiveShouldSetFirstMatchedMailboxWhenMultipleMailboxes(JMAPFilteringTestSystem
 testSystem) throws Exception {
+            InMemoryMailboxManager mailboxManager = 
testSystem.getMailboxManager();
+            MailboxId mailbox1Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_1");
+            MailboxId mailbox2Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_2");
+            MailboxId mailbox3Id = testSystem.createMailbox(mailboxManager, 
RECIPIENT_1_USERNAME, "RECIPIENT_1_MAILBOX_3");
+
+            
testSystem.getFilteringManagement().defineRulesForUser(User.fromUsername(RECIPIENT_1_USERNAME),
+                ImmutableList.of(
+                    Rule.builder()
+                        .id(Rule.Id.of("1"))
+                        .name("rule 1")
+                        .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of(
+                            mailbox3Id.serialize(),
+                            mailbox2Id.serialize(),
+                            mailbox1Id.serialize()))))
+                        .build()));
+
+            FakeMail mail = FakeMail.builder()
+                .sender(USER_1_ADDRESS)
+                .recipients(RECIPIENT_1)
+                .mimeMessage(mimeMessageBuilder()
+                    .setSubject(UNSCRAMBLED_SUBJECT))
+                .build();
+
+            testSystem.getJmapFiltering().service(mail);
+
+            assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isEqualTo("RECIPIENT_1_MAILBOX_3");
+        }
+
+        @Test
+        void 
mailDirectiveShouldNotBeSetWhenAllDoNotExactlyEqualsRuleValue(JMAPFilteringTestSystem
 testSystem) throws Exception {
+            testSystem.defineRulesForRecipient1(
+                Rule.Condition.of(FROM, CONTAINS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(FROM, EXACTLY_EQUALS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(TO, CONTAINS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(TO, EXACTLY_EQUALS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(CC, CONTAINS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(CC, EXACTLY_EQUALS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(RECIPIENT, EXACTLY_EQUALS, 
USER_1_FULL_ADDRESS),
+                Rule.Condition.of(RECIPIENT, EXACTLY_EQUALS, 
USER_1_FULL_ADDRESS),
+                Rule.Condition.of(SUBJECT, CONTAINS, USER_1_FULL_ADDRESS),
+                Rule.Condition.of(SUBJECT, EXACTLY_EQUALS, USER_1_FULL_ADDRESS)
+            );
+
+            FakeMail mail = testSystem.asMail(mimeMessageBuilder());
+            testSystem.getJmapFiltering().service(mail);
+
+            assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isNull();
+        }
+
+        @Test
+        void 
mailDirectiveShouldNotBeSetWhenNoneRulesValueIsContained(JMAPFilteringTestSystem
 testSystem) throws Exception {
+
+            testSystem.defineRulesForRecipient1(
+                Rule.Condition.of(FROM, CONTAINS, SHOULD_NOT_MATCH),
+                Rule.Condition.of(TO, CONTAINS, SHOULD_NOT_MATCH),
+                Rule.Condition.of(CC, CONTAINS, SHOULD_NOT_MATCH));
+
+            FakeMail mail = FakeMail.builder()
+                .sender(USER_1_ADDRESS)
+                .recipients(RECIPIENT_1)
+                .mimeMessage(mimeMessageBuilder()
+                    .addFrom(USER_1_FULL_ADDRESS)
+                    .addToRecipient(USER_2_FULL_ADDRESS)
+                    .addCcRecipient(USER_3_FULL_ADDRESS))
+                .build();
+
+            testSystem.getJmapFiltering().service(mail);
+
+            assertThat(mail.getAttribute(DELIVERY_PATH_PREFIX + 
RECIPIENT_1_USERNAME))
+                .isNull();
+        }
+    }
+}
\ No newline at end of file


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

Reply via email to