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

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


The following commit(s) were added to refs/heads/master by this push:
     new 76ac78344d JAMES-3908 implement JMAP filtering: combine rule 
conditions (#1643)
76ac78344d is described below

commit 76ac78344d489d2935b62d8cfefc5266eba3588d
Author: hungphan227 <45198168+hungphan...@users.noreply.github.com>
AuthorDate: Thu Jul 20 09:53:14 2023 +0700

    JAMES-3908 implement JMAP filtering: combine rule conditions (#1643)
---
 .../james/jmap/cassandra/filtering/RuleDTO.java    |  86 ++++++++++--
 .../james/jmap/cassandra/filtering/DTOTest.java    |  32 ++++-
 .../src/test/resources/json/event-v3.json          |  53 ++++++++
 .../src/test/resources/json/eventComplex-v3.json   |  99 ++++++++++++++
 .../src/test/resources/json/increment-v3.json      | 114 ++++++++++++++++
 .../org/apache/james/jmap/api/filtering/Rule.java  |  90 +++++++++++--
 .../james/jmap/api/filtering/RuleFixture.java      |  68 +++++-----
 .../apache/james/jmap/api/filtering/RuleTest.java  |  24 ++--
 .../impl/FilterUsernameChangeTaskStepTest.java     |   2 +-
 .../apache/james/jmap/draft/model/JmapRuleDTO.java |   8 +-
 .../james/jmap/mailet/filter/MailMatcher.java      |  75 ++++++++---
 .../jmap/mailet/filter/JMAPFilteringExtension.java |   2 +-
 .../jmap/mailet/filter/JMAPFilteringTest.java      | 150 +++++++++++++++------
 .../memory/MemoryUserDeletionIntegrationTest.java  |   2 +-
 .../MemoryUsernameChangeIntegrationTest.java       |   2 +-
 ...pulateFilteringProjectionRequestToTaskTest.java |   2 +-
 16 files changed, 668 insertions(+), 141 deletions(-)

diff --git 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/filtering/RuleDTO.java
 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/filtering/RuleDTO.java
index 86a39ac00a..a653d90e11 100644
--- 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/filtering/RuleDTO.java
+++ 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/filtering/RuleDTO.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap.cassandra.filtering;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 
 import org.apache.james.jmap.api.filtering.Rule;
 
@@ -32,6 +33,50 @@ import com.google.common.collect.ImmutableList;
 
 public class RuleDTO {
 
+    public static class ConditionGroupDTO {
+
+        private final Rule.ConditionCombiner conditionCombiner;
+        private final List<ConditionDTO> conditionDTOs;
+
+        @JsonCreator
+        public ConditionGroupDTO(@JsonProperty("conditionCombiner") 
Rule.ConditionCombiner conditionCombiner,
+                                 @JsonProperty("conditions") 
List<ConditionDTO> conditionDTOs) {
+            this.conditionCombiner = conditionCombiner;
+            this.conditionDTOs = conditionDTOs;
+        }
+
+        public Rule.ConditionCombiner getConditionCombiner() {
+            return conditionCombiner;
+        }
+
+        public List<ConditionDTO> getConditions() {
+            return conditionDTOs;
+        }
+
+        public Rule.ConditionGroup toConditionGroup() {
+            return Rule.ConditionGroup.of(conditionCombiner, 
conditionDTOs.stream().map(ConditionDTO::toCondition).collect(ImmutableList.toImmutableList()));
+        }
+
+        public static ConditionGroupDTO from(Rule.ConditionGroup 
conditionGroup) {
+            return new 
ConditionGroupDTO(conditionGroup.getConditionCombiner(), 
conditionGroup.getConditions().stream().map(ConditionDTO::from).collect(ImmutableList.toImmutableList()));
+        }
+
+        @Override
+        public final boolean equals(Object o) {
+            if (o instanceof ConditionGroupDTO) {
+                ConditionGroupDTO other = (ConditionGroupDTO) o;
+                return Objects.equals(conditionCombiner, 
other.conditionCombiner)
+                    && Objects.equals(conditionDTOs, other.conditionDTOs);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(conditionCombiner, conditionDTOs);
+        }
+    }
+
     public static class ConditionDTO {
 
         public static ConditionDTO from(Rule.Condition condition) {
@@ -215,24 +260,44 @@ public class RuleDTO {
     public static RuleDTO from(Rule rule) {
         return new RuleDTO(rule.getId().asString(),
                 rule.getName(),
-                ConditionDTO.from(rule.getCondition()),
+                ConditionGroupDTO.from(rule.getConditionGroup()),
                 ActionDTO.from(rule.getAction()));
     }
 
     private final String id;
     private final String name;
-    private final ConditionDTO conditionDTO;
+    private final ConditionGroupDTO conditionGroupDTO;
     private final ActionDTO actionDTO;
 
+    public RuleDTO(String id,
+                   String name,
+                   ConditionGroupDTO conditionGroupDTO,
+                   ActionDTO actionDTO) {
+        Preconditions.checkNotNull(id);
+
+        this.name = name;
+        this.conditionGroupDTO = conditionGroupDTO;
+        this.actionDTO = actionDTO;
+
+        this.id = id;
+    }
+
     @JsonCreator
     public RuleDTO(@JsonProperty("id") String id,
                    @JsonProperty("name") String name,
-                   @JsonProperty("condition") ConditionDTO conditionDTO,
+                   @JsonProperty("condition") Optional<ConditionDTO> 
conditionDTO,
+                   @JsonProperty("conditionGroup") Optional<ConditionGroupDTO> 
conditionGroupDTO,
                    @JsonProperty("action") ActionDTO actionDTO) {
+        Preconditions.checkNotNull(id);
+
         this.name = name;
-        this.conditionDTO = conditionDTO;
+        if (conditionGroupDTO.isPresent()) {
+            this.conditionGroupDTO = conditionGroupDTO.orElseThrow();
+        } else {
+            this.conditionGroupDTO = new 
ConditionGroupDTO(Rule.ConditionCombiner.AND,
+                ImmutableList.of(conditionDTO.orElseThrow(() -> new 
RuntimeException("Condition field in the rule with id " + id + " is 
missing"))));
+        }
         this.actionDTO = actionDTO;
-        Preconditions.checkNotNull(id);
 
         this.id = id;
     }
@@ -245,8 +310,8 @@ public class RuleDTO {
         return name;
     }
 
-    public ConditionDTO getCondition() {
-        return conditionDTO;
+    public ConditionGroupDTO getConditionGroup() {
+        return conditionGroupDTO;
     }
 
     public ActionDTO getAction() {
@@ -257,8 +322,7 @@ public class RuleDTO {
         return Rule.builder()
             .id(Rule.Id.of(id))
             .name(name)
-            .condition(conditionDTO.toCondition())
-            .name(name)
+            .conditionGroup(conditionGroupDTO.toConditionGroup())
             .action(actionDTO.toAction())
             .build();
     }
@@ -270,7 +334,7 @@ public class RuleDTO {
 
             return Objects.equals(this.id, ruleDTO.id)
                    && Objects.equals(this.name, ruleDTO.name)
-                   && Objects.equals(this.conditionDTO, ruleDTO.conditionDTO)
+                   && Objects.equals(this.conditionGroupDTO, 
ruleDTO.conditionGroupDTO)
                    && Objects.equals(this.actionDTO, ruleDTO.actionDTO);
         }
         return false;
@@ -278,7 +342,7 @@ public class RuleDTO {
 
     @Override
     public final int hashCode() {
-        return Objects.hash(id, name, conditionDTO, actionDTO);
+        return Objects.hash(id, name, conditionGroupDTO, actionDTO);
     }
 
     @Override
diff --git 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/filtering/DTOTest.java
 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/filtering/DTOTest.java
index 25ca063d4a..8a09eb880a 100644
--- 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/filtering/DTOTest.java
+++ 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/filtering/DTOTest.java
@@ -29,8 +29,6 @@ import static 
org.apache.james.jmap.cassandra.filtering.FilteringRuleSetDefineDT
 import static 
org.apache.james.jmap.cassandra.filtering.FilteringRuleSetDefineDTOModules.FILTERING_RULE_SET_DEFINED;
 import static org.assertj.core.api.Assertions.assertThat;
 
-import java.util.Optional;
-
 import org.apache.james.JsonSerializationVerifier;
 import org.apache.james.core.Username;
 import org.apache.james.eventsourcing.EventId;
@@ -50,9 +48,11 @@ import com.google.common.collect.ImmutableSet;
 class DTOTest {
     static final String EVENT_JSON = 
ClassLoaderUtils.getSystemResourceAsString("json/event.json");
     static final String EVENT_JSON_2 = 
ClassLoaderUtils.getSystemResourceAsString("json/event-v2.json");
+    static final String EVENT_JSON_3 = 
ClassLoaderUtils.getSystemResourceAsString("json/event-v3.json");
     static final String EVENT_EMPTY_JSON = 
ClassLoaderUtils.getSystemResourceAsString("json/eventEmpty.json");
     static final String EVENT_COMPLEX_JSON = 
ClassLoaderUtils.getSystemResourceAsString("json/eventComplex.json");
     static final String EVENT_COMPLEX_JSON_2 = 
ClassLoaderUtils.getSystemResourceAsString("json/eventComplex-v2.json");
+    static final String EVENT_COMPLEX_JSON_3 = 
ClassLoaderUtils.getSystemResourceAsString("json/eventComplex-v3.json");
 
     static final RuleSetDefined SIMPLE_RULE = new RuleSetDefined(
                     new FilteringAggregateId(Username.of("Bart")),
@@ -78,11 +78,23 @@ class DTOTest {
     void shouldSerializeRule() throws Exception {
         JsonSerializationVerifier.dtoModule(FILTERING_RULE_SET_DEFINED)
             .testCase(EMPTY_RULE, EVENT_EMPTY_JSON)
-            .testCase(SIMPLE_RULE, EVENT_JSON_2)
-            .testCase(COMPLEX_RULE, EVENT_COMPLEX_JSON_2)
+            .testCase(SIMPLE_RULE, EVENT_JSON_3)
+            .testCase(COMPLEX_RULE, EVENT_COMPLEX_JSON_3)
             .verify();
     }
 
+    @Test
+    void shouldDeserializeV2() {
+        JsonGenericSerializer<RuleSetDefined, FilteringRuleSetDefinedDTO> 
serializer = JsonGenericSerializer
+            .forModules(FILTERING_RULE_SET_DEFINED)
+            .withoutNestedType();
+
+        SoftAssertions.assertSoftly(Throwing.consumer(softly -> {
+            
softly.assertThat(serializer.deserialize(EVENT_JSON_2)).isEqualToComparingFieldByFieldRecursively(SIMPLE_RULE);
+            
softly.assertThat(serializer.deserialize(EVENT_COMPLEX_JSON_2)).isEqualToComparingFieldByFieldRecursively(COMPLEX_RULE);
+        }));
+    }
+
     @Test
     void shouldDeserializeV1() {
         JsonGenericSerializer<RuleSetDefined, FilteringRuleSetDefinedDTO> 
serializer = JsonGenericSerializer
@@ -98,10 +110,20 @@ class DTOTest {
     @Test
     void shouldSerializeIncrements() throws Exception {
         JsonSerializationVerifier.dtoModule(FILTERING_INCREMENT)
-            .testCase(INCREMENT, 
ClassLoaderUtils.getSystemResourceAsString("json/increment-v2.json"))
+            .testCase(INCREMENT, 
ClassLoaderUtils.getSystemResourceAsString("json/increment-v3.json"))
             .verify();
     }
 
+    @Test
+    void shouldDeserializeV2ForIncrements() throws Exception {
+        JsonGenericSerializer<IncrementalRuleChange, 
FilteringIncrementalRuleChangeDTO> serializer = JsonGenericSerializer
+            .forModules(FILTERING_INCREMENT)
+            .withoutNestedType();
+
+        
assertThat(serializer.deserialize(ClassLoaderUtils.getSystemResourceAsString("json/increment-v2.json")))
+            .isEqualToComparingFieldByFieldRecursively(INCREMENT);
+    }
+
     @Test
     void shouldDeserializeV1ForIncrements() throws Exception {
         JsonGenericSerializer<IncrementalRuleChange, 
FilteringIncrementalRuleChangeDTO> serializer = JsonGenericSerializer
diff --git 
a/server/data/data-jmap-cassandra/src/test/resources/json/event-v3.json 
b/server/data/data-jmap-cassandra/src/test/resources/json/event-v3.json
new file mode 100644
index 0000000000..699f289a80
--- /dev/null
+++ b/server/data/data-jmap-cassandra/src/test/resources/json/event-v3.json
@@ -0,0 +1,53 @@
+{
+  "type":"filtering-rule-set-defined",
+  "eventId":0,
+  "aggregateId":"FilteringRule/bart",
+  "rules":[
+    {
+      "id":"1",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "cc",
+            "comparator": "contains",
+            "value": "something"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["id-01"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    },
+    {
+      "id":"2",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "cc",
+            "comparator": "contains",
+            "value": "something"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["id-01"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git 
a/server/data/data-jmap-cassandra/src/test/resources/json/eventComplex-v3.json 
b/server/data/data-jmap-cassandra/src/test/resources/json/eventComplex-v3.json
new file mode 100644
index 0000000000..66cdae15f2
--- /dev/null
+++ 
b/server/data/data-jmap-cassandra/src/test/resources/json/eventComplex-v3.json
@@ -0,0 +1,99 @@
+{
+  "type":"filtering-rule-set-defined",
+  "eventId":0,
+  "aggregateId":"FilteringRule/bart",
+  "rules":[
+    {
+      "id":"id-from",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "from",
+            "comparator": "contains",
+            "value": "A value to match 4"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["mbx1"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    },
+    {
+      "id":"id-rcpt",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "recipient",
+            "comparator": "not-exactly-equals",
+            "value": "A value to match 3"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["mbx1"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    },
+    {
+      "id":"id-subject",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "subject",
+            "comparator": "not-contains",
+            "value": "A value to match 2"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["mbx1"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    },
+    {
+      "id":"id-to",
+      "name":"a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "to",
+            "comparator": "exactly-equals",
+            "value": "A value to match 1"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds":["mbx1"]
+        },
+        "important":false,
+        "keyworkds":[],
+        "reject":false,
+        "seen":false
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git 
a/server/data/data-jmap-cassandra/src/test/resources/json/increment-v3.json 
b/server/data/data-jmap-cassandra/src/test/resources/json/increment-v3.json
new file mode 100644
index 0000000000..65bfd0590f
--- /dev/null
+++ b/server/data/data-jmap-cassandra/src/test/resources/json/increment-v3.json
@@ -0,0 +1,114 @@
+{
+  "type": "filtering-increment",
+  "eventId": 0,
+  "aggregateId": "FilteringRule/bart",
+  "prepended": [
+    {
+      "id": "id-from",
+      "name": "a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "from",
+            "comparator": "contains",
+            "value": "A value to match 4"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds": [
+            "mbx1"
+          ]
+        },
+        "important": false,
+        "keyworkds": [],
+        "reject": false,
+        "seen": false
+      }
+    },
+    {
+      "id": "id-to",
+      "name": "a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "to",
+            "comparator": "exactly-equals",
+            "value": "A value to match 1"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds": [
+            "mbx1"
+          ]
+        },
+        "important": false,
+        "keyworkds": [],
+        "reject": false,
+        "seen": false
+      }
+    }
+  ],
+  "postpended": [
+    {
+      "id": "id-rcpt",
+      "name": "a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "recipient",
+            "comparator": "not-exactly-equals",
+            "value": "A value to match 3"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds": [
+            "mbx1"
+          ]
+        },
+        "important": false,
+        "keyworkds": [],
+        "reject": false,
+        "seen": false
+      }
+    }
+  ],
+  "updated": [
+    {
+      "id": "id-subject",
+      "name": "a name",
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "field": "subject",
+            "comparator": "not-contains",
+            "value": "A value to match 2"
+          }
+        ]
+      },
+      "action": {
+        "appendIn": {
+          "mailboxIds": [
+            "mbx1"
+          ]
+        },
+        "important": false,
+        "keyworkds": [],
+        "reject": false,
+        "seen": false
+      }
+    }
+  ],
+  "deleted": [
+    "abdcd"
+  ]
+}
\ No newline at end of file
diff --git 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/filtering/Rule.java
 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/filtering/Rule.java
index 91c79317fa..bed13dad21 100644
--- 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/filtering/Rule.java
+++ 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/filtering/Rule.java
@@ -72,6 +72,60 @@ public class Rule {
         }
     }
 
+    public static class ConditionGroup {
+
+        public static ConditionGroup of(ConditionCombiner conditionCombiner, 
List<Condition> conditions) {
+            return new ConditionGroup(conditionCombiner, conditions);
+        }
+
+        public static ConditionGroup of(ConditionCombiner conditionCombiner, 
Condition... conditions) {
+            return new ConditionGroup(conditionCombiner, 
ImmutableList.copyOf(conditions));
+        }
+
+        public static ConditionGroup of(Condition condition) {
+            return ConditionGroup.of(ConditionCombiner.AND, condition);
+        }
+
+        private final ConditionCombiner conditionCombiner;
+        private final List<Condition> conditions;
+
+        private ConditionGroup(ConditionCombiner conditionCombiner, 
List<Condition> conditions) {
+            this.conditionCombiner = conditionCombiner;
+            this.conditions = conditions;
+        }
+
+        public ConditionCombiner getConditionCombiner() {
+            return conditionCombiner;
+        }
+
+        public List<Condition> getConditions() {
+            return conditions;
+        }
+
+        @Override
+        public final boolean equals(Object o) {
+            if (o instanceof ConditionGroup) {
+                ConditionGroup other = (ConditionGroup) o;
+                return Objects.equals(conditionCombiner, 
other.conditionCombiner)
+                    && Objects.equals(conditions, other.conditions);
+            }
+            return false;
+        }
+
+        @Override
+        public final int hashCode() {
+            return Objects.hash(conditionCombiner, conditions);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                .add("conditionCombiner", conditionCombiner)
+                .add("conditions", conditions)
+                .toString();
+        }
+    }
+
     public static class Condition {
 
         public enum Field {
@@ -306,7 +360,7 @@ public class Rule {
 
         private Id id;
         private String name;
-        private Condition condition;
+        private ConditionGroup conditionGroup;
         private Action action;
 
         public Builder id(Id id) {
@@ -319,8 +373,13 @@ public class Rule {
             return this;
         }
 
-        public Builder condition(Condition condition) {
-            this.condition = condition;
+        public Builder conditionGroup(Condition condition) {
+            this.conditionGroup = Rule.ConditionGroup.of(condition);
+            return this;
+        }
+
+        public Builder conditionGroup(ConditionGroup conditionGroup) {
+            this.conditionGroup = conditionGroup;
             return this;
         }
 
@@ -332,10 +391,10 @@ public class Rule {
         public Rule build() {
             Preconditions.checkState(id != null, "`id` is mandatory");
             Preconditions.checkState(StringUtils.isNotBlank(name), "`name` is 
mandatory");
-            Preconditions.checkState(condition != null, "`condition` is 
mandatory");
+            Preconditions.checkState(conditionGroup != null, "`conditions` is 
mandatory");
             Preconditions.checkState(action != null, "`action` is mandatory");
 
-            return new Rule(id, name, condition, action);
+            return new Rule(id, name, conditionGroup, action);
         }
 
     }
@@ -344,15 +403,20 @@ public class Rule {
         return new Builder();
     }
 
+    public enum ConditionCombiner {
+        AND,
+        OR
+    }
+
     private final Id id;
     private final String name;
-    private final Condition condition;
+    private final ConditionGroup conditionGroup;
     private final Action action;
 
-    private Rule(Id id, String name, Condition condition, Action action) {
+    private Rule(Id id, String name, ConditionGroup conditionGroup, Action 
action) {
         this.id = id;
         this.name = name;
-        this.condition = condition;
+        this.conditionGroup = conditionGroup;
         this.action = action;
     }
 
@@ -364,8 +428,8 @@ public class Rule {
         return name;
     }
 
-    public Condition getCondition() {
-        return condition;
+    public ConditionGroup getConditionGroup() {
+        return conditionGroup;
     }
 
     public Action getAction() {
@@ -379,7 +443,7 @@ public class Rule {
 
             return Objects.equals(this.id, rule.id)
                 && Objects.equals(this.name, rule.name)
-                && Objects.equals(this.condition, rule.condition)
+                && Objects.equals(this.conditionGroup, rule.conditionGroup)
                 && Objects.equals(this.action, rule.action);
         }
         return false;
@@ -387,7 +451,7 @@ public class Rule {
 
     @Override
     public final int hashCode() {
-        return Objects.hash(id, name, condition, action);
+        return Objects.hash(id, name, conditionGroup, action);
     }
 
     @Override
@@ -395,7 +459,7 @@ public class Rule {
         return MoreObjects.toStringHelper(this)
             .add("id", id)
             .add("name", name)
-            .add("condition", condition)
+            .add("conditionGroup", conditionGroup)
             .add("action", action)
             .toString();
     }
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleFixture.java
 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleFixture.java
index d39bb25d2c..256070dc54 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleFixture.java
+++ 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleFixture.java
@@ -23,10 +23,10 @@ public interface RuleFixture {
     String NAME = "a name";
     Rule.Condition CONDITION = Rule.Condition.of(Rule.Condition.Field.CC, 
Rule.Condition.Comparator.CONTAINS, "something");
     Rule.Action ACTION = 
Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("id-01"));
-    Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).condition(CONDITION).action(ACTION);
+    Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).conditionGroup(CONDITION).action(ACTION);
     Rule RULE_1 = RULE_BUILDER.id(Rule.Id.of("1")).build();
     Rule RULE_1_MODIFIED = Rule.builder()
-        .condition(CONDITION)
+        .conditionGroup(CONDITION)
         .action(ACTION)
         .id(Rule.Id.of("1"))
         .name("newname")
@@ -35,43 +35,43 @@ public interface RuleFixture {
     Rule RULE_3 = RULE_BUILDER.id(Rule.Id.of("3")).build();
 
     Rule RULE_TO = Rule.builder()
-            .id(Rule.Id.of("id-to"))
-            .name(NAME)
-            
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
-            .condition(Rule.Condition.of(
-                    Rule.Condition.Field.TO,
-                    Rule.Condition.Comparator.EXACTLY_EQUALS,
-                    "A value to match 1"))
-            .build();
+        .id(Rule.Id.of("id-to"))
+        .name(NAME)
+        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
+        .conditionGroup(Rule.Condition.of(
+            Rule.Condition.Field.TO,
+            Rule.Condition.Comparator.EXACTLY_EQUALS,
+            "A value to match 1"))
+        .build();
 
     Rule RULE_SUBJECT = Rule.builder()
-            .id(Rule.Id.of("id-subject"))
-            .name(NAME)
-            
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
-            .condition(Rule.Condition.of(
-                    Rule.Condition.Field.SUBJECT,
-                    Rule.Condition.Comparator.NOT_CONTAINS,
-                    "A value to match 2"))
-            .build();
+        .id(Rule.Id.of("id-subject"))
+        .name(NAME)
+        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
+        .conditionGroup(Rule.Condition.of(
+            Rule.Condition.Field.SUBJECT,
+            Rule.Condition.Comparator.NOT_CONTAINS,
+            "A value to match 2"))
+        .build();
 
     Rule RULE_RECIPIENT = Rule.builder()
-            .id(Rule.Id.of("id-rcpt"))
-            .name(NAME)
-            
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
-            .condition(Rule.Condition.of(
-                    Rule.Condition.Field.RECIPIENT,
-                    Rule.Condition.Comparator.NOT_EXACTLY_EQUALS,
-                    "A value to match 3"))
-            .build();
+        .id(Rule.Id.of("id-rcpt"))
+        .name(NAME)
+        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
+        .conditionGroup(Rule.Condition.of(
+            Rule.Condition.Field.RECIPIENT,
+            Rule.Condition.Comparator.NOT_EXACTLY_EQUALS,
+            "A value to match 3"))
+        .build();
 
     Rule RULE_FROM = Rule.builder()
-            .id(Rule.Id.of("id-from"))
-            .name(NAME)
-            
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
-            .condition(Rule.Condition.of(
-                    Rule.Condition.Field.FROM,
-                    Rule.Condition.Comparator.CONTAINS,
-                    "A value to match 4"))
-            .build();
+        .id(Rule.Id.of("id-from"))
+        .name(NAME)
+        
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("mbx1")))
+        .conditionGroup(Rule.Condition.of(
+            Rule.Condition.Field.FROM,
+            Rule.Condition.Comparator.CONTAINS,
+            "A value to match 4"))
+        .build();
 
 }
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleTest.java
 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleTest.java
index 830f975b23..37dc1263bf 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleTest.java
+++ 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/RuleTest.java
@@ -51,6 +51,12 @@ class RuleTest {
             .verify();
     }
 
+    @Test
+    void innerClassConditionGroupShouldMatchBeanContract() {
+        EqualsVerifier.forClass(Rule.ConditionGroup.class)
+            .verify();
+    }
+
     @Test
     void innerClassActionShouldMatchBeanContract() {
         EqualsVerifier.forClass(Rule.Action.class)
@@ -89,7 +95,7 @@ class RuleTest {
         assertThatThrownBy(() ->
             Rule.builder()
                 .name(NAME)
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .action(ACTION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
@@ -100,7 +106,7 @@ class RuleTest {
         assertThatThrownBy(() ->
             Rule.builder()
                 .id(UNIQUE_ID)
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .action(ACTION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
@@ -112,7 +118,7 @@ class RuleTest {
             Rule.builder()
                 .id(UNIQUE_ID)
                 .name("")
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .action(ACTION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
@@ -124,7 +130,7 @@ class RuleTest {
             Rule.builder()
                 .id(UNIQUE_ID)
                 .name("    ")
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .action(ACTION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
@@ -136,7 +142,7 @@ class RuleTest {
             Rule.builder()
                 .id(UNIQUE_ID)
                 .name(null)
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .action(ACTION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
@@ -159,7 +165,7 @@ class RuleTest {
             Rule.builder()
                 .id(UNIQUE_ID)
                 .name(NAME)
-                .condition(CONDITION)
+                .conditionGroup(CONDITION)
                 .build())
             .isInstanceOf(IllegalStateException.class);
     }
@@ -169,11 +175,11 @@ class RuleTest {
         Rule rule = Rule.builder()
             .id(UNIQUE_ID)
             .name(NAME)
-            .condition(CONDITION)
+            .conditionGroup(CONDITION)
             .action(ACTION)
             .build();
 
-        assertThat(rule.getCondition()).isEqualTo(CONDITION);
+        
assertThat(rule.getConditionGroup().getConditions().get(0)).isEqualTo(CONDITION);
     }
 
     @Test
@@ -181,7 +187,7 @@ class RuleTest {
         Rule rule = Rule.builder()
             .id(UNIQUE_ID)
             .name(NAME)
-            .condition(CONDITION)
+            .conditionGroup(CONDITION)
             .action(ACTION)
             .build();
 
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/impl/FilterUsernameChangeTaskStepTest.java
 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/impl/FilterUsernameChangeTaskStepTest.java
index 6a4c89a289..3ef04bdba1 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/impl/FilterUsernameChangeTaskStepTest.java
+++ 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/filtering/impl/FilterUsernameChangeTaskStepTest.java
@@ -44,7 +44,7 @@ public class FilterUsernameChangeTaskStepTest {
     private static final String NAME = "a name";
     private static final Rule.Condition CONDITION = 
Rule.Condition.of(Rule.Condition.Field.CC, Rule.Condition.Comparator.CONTAINS, 
"something");
     private static final Rule.Action ACTION = 
Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("id-01"));
-    private static final Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).condition(CONDITION).action(ACTION);
+    private static final Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).conditionGroup(CONDITION).action(ACTION);
     private static final Rule RULE_1 = 
RULE_BUILDER.id(Rule.Id.of("1")).build();
     private static final Rule RULE_2 = 
RULE_BUILDER.id(Rule.Id.of("2")).build();
 
diff --git 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/JmapRuleDTO.java
 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/JmapRuleDTO.java
index a4e2478dea..1823766f02 100644
--- 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/JmapRuleDTO.java
+++ 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/JmapRuleDTO.java
@@ -136,9 +136,9 @@ public class JmapRuleDTO {
 
     public static JmapRuleDTO from(Rule rule) {
         return new JmapRuleDTO(rule.getId().asString(),
-                rule.getName(),
-                ConditionDTO.from(rule.getCondition()),
-                ActionDTO.from(rule.getAction()));
+            rule.getName(),
+            ConditionDTO.from(rule.getConditionGroup().getConditions().get(0)),
+            ActionDTO.from(rule.getAction()));
     }
 
     private final String id;
@@ -177,7 +177,7 @@ public class JmapRuleDTO {
         return Rule.builder()
             .id(Rule.Id.of(id))
             .name(name)
-            .condition(conditionDTO.toCondition())
+            .conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, 
ImmutableList.of(conditionDTO.toCondition())))
             .action(actionDTO.toAction())
             .build();
     }
diff --git 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
index ed5db25a33..f583561cad 100644
--- 
a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
+++ 
b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
@@ -19,9 +19,7 @@
 
 package org.apache.james.jmap.mailet.filter;
 
-import static org.apache.james.jmap.api.filtering.Rule.Condition;
-
-import java.util.Optional;
+import java.util.List;
 import java.util.stream.Stream;
 
 import org.apache.james.jmap.api.filtering.Rule;
@@ -30,6 +28,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 
 public interface MailMatcher {
 
@@ -37,11 +37,43 @@ public interface MailMatcher {
 
         private static final Logger LOGGER = 
LoggerFactory.getLogger(HeaderMatcher.class);
 
+        private List<MailMatchingCondition> mailMatchingConditions;
+        private final Rule.ConditionCombiner conditionCombiner;
+
+        private HeaderMatcher(List<MailMatchingCondition> 
mailMatchingConditions, Rule.ConditionCombiner conditionCombiner) {
+            this.mailMatchingConditions = mailMatchingConditions;
+            this.conditionCombiner = conditionCombiner;
+        }
+
+        @Override
+        public boolean match(Mail mail) {
+            try {
+                Predicate<MailMatchingCondition> predicate = 
(MailMatchingCondition mailMatchingCondition) -> {
+                    Stream<String> headerLines = 
mailMatchingCondition.getHeaderExtractor().apply(mail);
+                    return 
mailMatchingCondition.getContentMatcher().match(headerLines, 
mailMatchingCondition.getRuleValue());
+                };
+
+                switch (conditionCombiner) {
+                    case AND:
+                        return 
mailMatchingConditions.stream().allMatch(predicate);
+                    case OR:
+                        return 
mailMatchingConditions.stream().anyMatch(predicate);
+                    default:
+                        throw new Exception(conditionCombiner + " 
conditionCombiner is not supported");
+                }
+            } catch (Exception e) {
+                LOGGER.error("error while extracting mail header", e);
+                return false;
+            }
+        }
+    }
+
+    class MailMatchingCondition {
         private final ContentMatcher contentMatcher;
         private final String ruleValue;
         private final HeaderExtractor headerExtractor;
 
-        private HeaderMatcher(ContentMatcher contentMatcher, String ruleValue,
+        private MailMatchingCondition(ContentMatcher contentMatcher, String 
ruleValue,
                               HeaderExtractor headerExtractor) {
             Preconditions.checkNotNull(contentMatcher);
             Preconditions.checkNotNull(headerExtractor);
@@ -51,27 +83,28 @@ public interface MailMatcher {
             this.headerExtractor = headerExtractor;
         }
 
-        @Override
-        public boolean match(Mail mail) {
-            try {
-                Stream<String> headerLines = headerExtractor.apply(mail);
-                return contentMatcher.match(headerLines, ruleValue);
-            } catch (Exception e) {
-                LOGGER.error("error while extracting mail header", e);
-                return false;
-            }
+        public ContentMatcher getContentMatcher() {
+            return contentMatcher;
+        }
+
+        public String getRuleValue() {
+            return ruleValue;
+        }
+
+        public HeaderExtractor getHeaderExtractor() {
+            return headerExtractor;
         }
     }
 
     static MailMatcher from(Rule rule) {
-        Condition ruleCondition = rule.getCondition();
-        Optional<ContentMatcher> maybeContentMatcher = 
ContentMatcher.asContentMatcher(ruleCondition.getField(), 
ruleCondition.getComparator());
-        Optional<HeaderExtractor> maybeHeaderExtractor = 
HeaderExtractor.asHeaderExtractor(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())));
+        return new 
HeaderMatcher(rule.getConditionGroup().getConditions().stream()
+            .map(ruleCondition -> new MailMatchingCondition(
+                ContentMatcher.asContentMatcher(ruleCondition.getField(), 
ruleCondition.getComparator())
+                    .orElseThrow(() -> new RuntimeException("No content 
matcher associated with field " + ruleCondition.getField())),
+                ruleCondition.getValue(),
+                HeaderExtractor.asHeaderExtractor(ruleCondition.getField())
+                    .orElseThrow(() -> new RuntimeException("No content 
matcher associated with comparator " + ruleCondition.getComparator())))
+            ).collect(ImmutableList.toImmutableList()), 
rule.getConditionGroup().getConditionCombiner());
     }
 
     boolean match(Mail mail);
diff --git 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
index 3696092b1b..64e1f93c00 100644
--- 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
+++ 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringExtension.java
@@ -112,7 +112,7 @@ public class JMAPFilteringExtension implements 
BeforeEachCallback, ParameterReso
                 .map(condition -> Rule.builder()
                     .id(Rule.Id.of(String.valueOf(counter.incrementAndGet())))
                     .name(String.valueOf(counter.incrementAndGet()))
-                    .condition(condition)
+                    .conditionGroup(condition)
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
                     .build())
                 .collect(ImmutableList.toImmutableList());
diff --git 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
index a4c1390aa4..df92049b28 100644
--- 
a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
+++ 
b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/filter/JMAPFilteringTest.java
@@ -46,6 +46,7 @@ import static 
org.apache.james.jmap.mailet.filter.JMAPFilteringFixture.USER_1_FU
 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_2_USERNAME;
 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_4_FULL_ADDRESS;
@@ -98,7 +99,7 @@ class JMAPFilteringTest {
         private Optional<Rule.Condition.Field> field;
         private MimeMessageBuilder mimeMessageBuilder;
         private Optional<String> valueToMatch;
-        
+
         private FilteringArgumentBuilder() {
             this.description = Optional.empty();
             this.field = Optional.empty();
@@ -178,9 +179,9 @@ class JMAPFilteringTest {
 
         public FilteringArgumentBuilder 
unscrambledSubjectShouldNotMatchCaseSensitive() {
             return description("unscrambled content (case sensitive)")
-                    .field(SUBJECT)
-                    .subject(UNSCRAMBLED_SUBJECT)
-                    
.valueToMatch(UNSCRAMBLED_SUBJECT.toUpperCase(Locale.FRENCH));
+                .field(SUBJECT)
+                .subject(UNSCRAMBLED_SUBJECT)
+                .valueToMatch(UNSCRAMBLED_SUBJECT.toUpperCase(Locale.FRENCH));
         }
 
         public FilteringArgumentBuilder testForUpperCase() {
@@ -192,7 +193,7 @@ class JMAPFilteringTest {
             Preconditions.checkState(description.isPresent());
             Preconditions.checkState(field.isPresent());
             Preconditions.checkState(valueToMatch.isPresent());
-            
+
             return Arguments.of(description.get(), field.get(), 
mimeMessageBuilder, valueToMatch.get());
         }
     }
@@ -599,23 +600,23 @@ class JMAPFilteringTest {
         testSystem.getJmapFiltering().service(mail);
 
         
assertThatAttribute(mail.getAttribute(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME))
-                .isEqualTo(RECIPIENT_1_MAILBOX_1_ATTRIBUTE);
+            .isEqualTo(RECIPIENT_1_MAILBOX_1_ATTRIBUTE);
     }
 
     @ParameterizedTest(name = "CONTAINS should not match for field {1}: {0}")
     @MethodSource("notContainsTestSuite")
     void notMatchingContainsTest(String testDescription,
-                              Rule.Condition.Field fieldToMatch,
-                              MimeMessageBuilder mimeMessageBuilder,
-                              String valueToMatch,
-                              JMAPFilteringTestSystem testSystem) throws 
Exception {
+                                 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(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME))
-                .isEmpty();
+            .isEmpty();
     }
 
     @ParameterizedTest(name = "NOT-CONTAINS should match for field {1}: {0}")
@@ -669,10 +670,10 @@ class JMAPFilteringTest {
     @ParameterizedTest(name = "EXACTLY-EQUALS should not match for field {1}: 
{0}")
     @MethodSource("notEqualsTestSuite")
     void equalsNotMatchingTest(String testDescription,
-                            Rule.Condition.Field fieldToMatch,
-                            MimeMessageBuilder mimeMessageBuilder,
-                            String valueToMatch,
-                            JMAPFilteringTestSystem testSystem) throws 
Exception {
+                               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);
@@ -725,26 +726,26 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                    .conditionGroup(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))
+                    .conditionGroup(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))
+                    .conditionGroup(Rule.Condition.of(TO, EXACTLY_EQUALS, 
USER_3_ADDRESS))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(mailbox3Id.serialize())))
                     .build())).block();
 
             FakeMail mail = testSystem.asMail(mimeMessageBuilder()
-                    .addFrom(USER_2_ADDRESS)
-                    .addToRecipient(USER_3_ADDRESS)
-                    .setSubject(UNSCRAMBLED_SUBJECT));
+                .addFrom(USER_2_ADDRESS)
+                .addToRecipient(USER_3_ADDRESS)
+                .setSubject(UNSCRAMBLED_SUBJECT));
 
             testSystem.getJmapFiltering().service(mail);
 
@@ -763,7 +764,7 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                    .conditionGroup(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of(
                         mailbox3Id.serialize(),
                         mailbox2Id.serialize(),
@@ -771,7 +772,7 @@ class JMAPFilteringTest {
                     .build())).block();
 
             FakeMail mail = testSystem.asMail(mimeMessageBuilder()
-                    .setSubject(UNSCRAMBLED_SUBJECT));
+                .setSubject(UNSCRAMBLED_SUBJECT));
 
             testSystem.getJmapFiltering().service(mail);
 
@@ -790,19 +791,19 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                    .conditionGroup(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of())))
                     .build(),
                 Rule.builder()
                     .id(Rule.Id.of("2"))
                     .name("rule 2")
-                    .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+                    .conditionGroup(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of(
                         mailbox1Id.serialize()))))
                     .build())).block();
 
             FakeMail mail = testSystem.asMail(mimeMessageBuilder()
-                    .setSubject(UNSCRAMBLED_SUBJECT));
+                .setSubject(UNSCRAMBLED_SUBJECT));
 
             testSystem.getJmapFiltering().service(mail);
 
@@ -839,9 +840,9 @@ class JMAPFilteringTest {
                 Rule.Condition.of(CC, CONTAINS, SHOULD_NOT_MATCH));
 
             FakeMail mail = testSystem.asMail(mimeMessageBuilder()
-                    .addFrom(USER_1_FULL_ADDRESS)
-                    .addToRecipient(USER_2_FULL_ADDRESS)
-                    .addCcRecipient(USER_3_FULL_ADDRESS));
+                .addFrom(USER_1_FULL_ADDRESS)
+                .addToRecipient(USER_2_FULL_ADDRESS)
+                .addCcRecipient(USER_3_FULL_ADDRESS));
 
             testSystem.getJmapFiltering().service(mail);
 
@@ -860,7 +861,7 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                    .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(unknownMailboxId)))
                     .build())).block();
 
@@ -879,7 +880,7 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                    .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(unknownMailboxId)))
                     .build())).block();
 
@@ -900,13 +901,13 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                    .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(unknownMailboxId)))
                     .build(),
                 Rule.builder()
                     .id(Rule.Id.of("2"))
                     .name("rule 2")
-                    .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                    .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(
                         testSystem.getRecipient1MailboxId().serialize())))
                     .build())).block();
@@ -929,7 +930,7 @@ class JMAPFilteringTest {
                 Rule.builder()
                     .id(Rule.Id.of("1"))
                     .name("rule 1")
-                    .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                    .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                     
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(
                         unknownMailboxId,
                         testSystem.getRecipient1MailboxId().serialize())))
@@ -952,7 +953,7 @@ class JMAPFilteringTest {
             Rule.builder()
                 .id(Rule.Id.of("1"))
                 .name("rule 1")
-                .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                 
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(),
                     false, false, true, ImmutableList.of()))
                 .build())).block();
@@ -972,7 +973,7 @@ class JMAPFilteringTest {
             Rule.builder()
                 .id(Rule.Id.of("1"))
                 .name("rule 1")
-                .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                 
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(),
                     true, false, false, ImmutableList.of()))
                 .build())).block();
@@ -995,7 +996,7 @@ class JMAPFilteringTest {
             Rule.builder()
                 .id(Rule.Id.of("1"))
                 .name("rule 1")
-                .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                 
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(),
                     false, true, false, ImmutableList.of()))
                 .build())).block();
@@ -1018,7 +1019,7 @@ class JMAPFilteringTest {
             Rule.builder()
                 .id(Rule.Id.of("1"))
                 .name("rule 1")
-                .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                 
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(),
                     false, false, false, ImmutableList.of("abc", "def")))
                 .build())).block();
@@ -1039,7 +1040,7 @@ class JMAPFilteringTest {
             Rule.builder()
                 .id(Rule.Id.of("1"))
                 .name("rule 1")
-                .condition(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
+                .conditionGroup(Rule.Condition.of(FROM, CONTAINS, 
FRED_MARTIN_FULLNAME))
                 
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(),
                     true, true, false, ImmutableList.of("abc", "def")))
                 .build())).block();
@@ -1057,4 +1058,75 @@ class JMAPFilteringTest {
         assertThat(StorageDirective.fromMail(Username.of("recipient1"), 
mail).getFlags().get())
             .isEqualTo(expectedFlags);
     }
+
+    @Test
+    void andShouldMatchWhenAllConditionsAreMet(JMAPFilteringTestSystem 
testSystem) throws Exception {
+        
Mono.from(testSystem.getFilteringManagement().defineRulesForUser(RECIPIENT_1_USERNAME,
+            Optional.empty(),
+            Rule.builder()
+                .id(Rule.Id.of("1"))
+                .name("rule 1")
+                
.conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, 
Rule.Condition.of(FROM, CONTAINS, USER_2_USERNAME), Rule.Condition.of(SUBJECT, 
CONTAINS, "cd")))
+                
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
+                .build())).block();
+
+        FakeMail mail = 
testSystem.asMail(mimeMessageBuilder().addHeader(FROM.asString(), 
USER_2_ADDRESS).addHeader(SUBJECT.asString(), "abcdef"));
+
+        testSystem.getJmapFiltering().service(mail);
+
+        
assertThatAttribute(mail.getAttribute(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME))
+            .isEqualTo(RECIPIENT_1_MAILBOX_1_ATTRIBUTE);
+    }
+
+    @Test
+    void andShouldNotMatchWhenSomeConditionsAreNotMet(JMAPFilteringTestSystem 
testSystem) throws Exception {
+        
Mono.from(testSystem.getFilteringManagement().defineRulesForUser(RECIPIENT_1_USERNAME,
+            Optional.empty(),
+            Rule.builder()
+                .id(Rule.Id.of("1"))
+                .name("rule 1")
+                
.conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, 
Rule.Condition.of(FROM, CONTAINS, USER_2_USERNAME), Rule.Condition.of(SUBJECT, 
CONTAINS, "cdf")))
+                
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
+                .build())).block();
+
+        FakeMail mail = 
testSystem.asMail(mimeMessageBuilder().addHeader(FROM.asString(), 
USER_2_ADDRESS).addHeader(SUBJECT.asString(), "abcdef"));
+        testSystem.getJmapFiltering().service(mail);
+
+        
assertThat(mail.getAttribute(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME).isEmpty()).isTrue();
+    }
+
+    @Test
+    void orShouldMatchWhenSomeConditionsAreMet(JMAPFilteringTestSystem 
testSystem) throws Exception {
+        
Mono.from(testSystem.getFilteringManagement().defineRulesForUser(RECIPIENT_1_USERNAME,
+            Optional.empty(),
+            Rule.builder()
+                .id(Rule.Id.of("1"))
+                .name("rule 1")
+                
.conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.OR, 
Rule.Condition.of(FROM, CONTAINS, USER_2_USERNAME), Rule.Condition.of(SUBJECT, 
CONTAINS, "cdf")))
+                
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
+                .build())).block();
+
+        FakeMail mail = 
testSystem.asMail(mimeMessageBuilder().addHeader(FROM.asString(), 
USER_2_ADDRESS).addHeader(SUBJECT.asString(), "abcdef"));
+        testSystem.getJmapFiltering().service(mail);
+
+        
assertThatAttribute(mail.getAttribute(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME))
+            .isEqualTo(RECIPIENT_1_MAILBOX_1_ATTRIBUTE);
+    }
+
+    @Test
+    void orShouldNotMatchWhenNoConditionsAreMet(JMAPFilteringTestSystem 
testSystem) throws Exception {
+        
Mono.from(testSystem.getFilteringManagement().defineRulesForUser(RECIPIENT_1_USERNAME,
+            Optional.empty(),
+            Rule.builder()
+                .id(Rule.Id.of("1"))
+                .name("rule 1")
+                
.conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.OR, 
Rule.Condition.of(FROM, CONTAINS, USER_2_USERNAME), Rule.Condition.of(SUBJECT, 
CONTAINS, "cdf")))
+                
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(testSystem.getRecipient1MailboxId().serialize())))
+                .build())).block();
+
+        FakeMail mail = 
testSystem.asMail(mimeMessageBuilder().addHeader(FROM.asString(), 
USER_3_ADDRESS).addHeader(SUBJECT.asString(), "abcdef"));
+        testSystem.getJmapFiltering().service(mail);
+
+        
assertThat(mail.getAttribute(RECIPIENT_1_USERNAME_ATTRIBUTE_NAME).isEmpty()).isTrue();
+    }
 }
\ No newline at end of file
diff --git 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
index 6fc450b308..ecada4e995 100644
--- 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
+++ 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
@@ -244,7 +244,7 @@ class MemoryUserDeletionIntegrationTest {
     void shouldDeleteFilters(GuiceJamesServer server) {
         Rule.Condition CONDITION = Rule.Condition.of(Rule.Condition.Field.CC, 
Rule.Condition.Comparator.CONTAINS, "something");
         Rule.Action ACTION = 
Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("id-01"));
-        Rule.Builder RULE_BUILDER = Rule.builder().name("A 
name").condition(CONDITION).action(ACTION);
+        Rule.Builder RULE_BUILDER = Rule.builder().name("A 
name").conditionGroup(CONDITION).action(ACTION);
         Rule RULE_1 = RULE_BUILDER.id(Rule.Id.of("1")).build();
         server.getProbe(MemoryUserDeletionIntegrationTestProbe.class)
             .defineFilters(ALICE, ImmutableList.of(RULE_1));
diff --git 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
index bd0ea5e116..dc3e65741f 100644
--- 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
+++ 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
@@ -108,7 +108,7 @@ class MemoryUsernameChangeIntegrationTest {
     private static final String NAME = "a name";
     private static final Rule.Condition CONDITION = 
Rule.Condition.of(Rule.Condition.Field.CC, Rule.Condition.Comparator.CONTAINS, 
"something");
     private static final Rule.Action ACTION = 
Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("id-01"));
-    private static final Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).condition(CONDITION).action(ACTION);
+    private static final Rule.Builder RULE_BUILDER = 
Rule.builder().name(NAME).conditionGroup(CONDITION).action(ACTION);
     private static final Rule RULE_1 = 
RULE_BUILDER.id(Rule.Id.of("1")).build();
     private static final Rule RULE_2 = 
RULE_BUILDER.id(Rule.Id.of("2")).build();
     private static final Optional<Version> NO_VERSION = Optional.empty();
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateFilteringProjectionRequestToTaskTest.java
 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateFilteringProjectionRequestToTaskTest.java
index 21546d1c5a..9e2171c266 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateFilteringProjectionRequestToTaskTest.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/PopulateFilteringProjectionRequestToTaskTest.java
@@ -210,7 +210,7 @@ class PopulateFilteringProjectionRequestToTaskTest {
         Rule rule = Rule.builder()
             .id(Rule.Id.of("2"))
             .name("rule 2")
-            .condition(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
+            .conditionGroup(Rule.Condition.of(SUBJECT, CONTAINS, 
UNSCRAMBLED_SUBJECT))
             
.action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of(bobInboxboxId.serialize()))))
             .build();
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to