You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2017/08/25 08:32:39 UTC

[01/13] james-project git commit: JAMES-2110 Define keyword on JMAP

Repository: james-project
Updated Branches:
  refs/heads/master 5accc88a1 -> 10d76ab6e


JAMES-2110 Define keyword on JMAP


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

Branch: refs/heads/master
Commit: 20beee7a17946b42d3464269af383338d76a463f
Parents: 5accc88
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:31:39 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:26 2017 +0200

----------------------------------------------------------------------
 .../org/apache/james/jmap/model/Keyword.java    | 126 +++++++
 .../org/apache/james/jmap/model/Keywords.java   | 202 ++++++++++++
 .../org/apache/james/jmap/model/OldKeyword.java |  92 ++++++
 .../apache/james/jmap/model/KeywordTest.java    | 189 +++++++++++
 .../apache/james/jmap/model/KeywordsTest.java   | 328 +++++++++++++++++++
 .../apache/james/jmap/model/OldKeywordTest.java |  31 ++
 6 files changed, 968 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keyword.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keyword.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keyword.java
new file mode 100644
index 0000000..ff1a2f0
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keyword.java
@@ -0,0 +1,126 @@
+/****************************************************************
+ * 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.model;
+
+import java.util.Optional;
+import javax.mail.Flags;
+
+import org.apache.commons.lang.StringUtils;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+
+public class Keyword {
+    private final static int FLAG_NAME_MIN_LENGTH = 1;
+    private final static int FLAG_NAME_MAX_LENGTH = 255;
+    private final static CharMatcher FLAG_NAME_PATTERN = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.is('$'));
+
+    public final static Keyword DRAFT = new Keyword("$Draft");
+    public final static Keyword SEEN = new Keyword("$Seen");
+    public final static Keyword FLAGGED = new Keyword("$Flagged");
+    public final static Keyword ANSWERED = new Keyword("$Answered");
+    public final static Keyword DELETED = new Keyword("$Deleted");
+    public final static Keyword RECENT = new Keyword("$Recent");
+    public final static Boolean FLAG_VALUE = true;
+
+    private final static ImmutableList<Keyword> NON_EXPOSED_IMAP_KEYWORDS = ImmutableList.of(Keyword.RECENT, Keyword.DELETED);
+    private final static ImmutableBiMap<Flags.Flag, Keyword> IMAP_SYSTEM_FLAGS = ImmutableBiMap.<Flags.Flag, Keyword>builder()
+        .put(Flags.Flag.DRAFT, DRAFT)
+        .put(Flags.Flag.SEEN, SEEN)
+        .put(Flags.Flag.FLAGGED, FLAGGED)
+        .put(Flags.Flag.ANSWERED, ANSWERED)
+        .put(Flags.Flag.RECENT, RECENT)
+        .put(Flags.Flag.DELETED, DELETED)
+        .build();
+
+    private final String flagName;
+
+    public static Keyword fromFlag(Flags.Flag flag) {
+        return IMAP_SYSTEM_FLAGS.get(flag);
+    }
+
+    public Keyword(String flagName) {
+        Preconditions.checkArgument(isValid(flagName),
+                "Flagname must not be null or empty, must have length form 1-255, must not contain charater with hex from '\u0000' to '\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ");
+        this.flagName = flagName;
+    }
+
+    private boolean isValid(String flagName) {
+        if (StringUtils.isBlank(flagName)) {
+            return false;
+        }
+        if (flagName.length() < FLAG_NAME_MIN_LENGTH || flagName.length() > FLAG_NAME_MAX_LENGTH) {
+            return false;
+        }
+        if (!FLAG_NAME_PATTERN.matchesAllOf(flagName)) {
+            return false;
+        }
+        return true;
+    }
+
+    public String getFlagName() {
+        return flagName;
+    }
+
+    public boolean isExposedImapKeyword() {
+        return !NON_EXPOSED_IMAP_KEYWORDS.contains(this);
+    }
+
+    public boolean isDraft() {
+        return DRAFT.equals(this);
+    }
+
+    public Optional<Flags.Flag> asSystemFlag() {
+        return Optional.ofNullable(IMAP_SYSTEM_FLAGS.inverse()
+            .get(this));
+    }
+
+    public Flags asFlags() {
+        return asSystemFlag()
+            .map(Flags::new)
+            .orElse(new Flags(flagName));
+    }
+
+    @Override
+    public final boolean equals(Object other) {
+        if (other instanceof Keyword) {
+            Keyword otherKeyword = (Keyword) other;
+            return Objects.equal(flagName, otherKeyword.flagName);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hashCode(flagName);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("flagName", flagName)
+            .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keywords.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keywords.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keywords.java
new file mode 100644
index 0000000..91aff31
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Keywords.java
@@ -0,0 +1,202 @@
+/****************************************************************
+ * 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.model;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import javax.mail.Flags;
+
+import org.apache.james.mailbox.FlagsBuilder;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class Keywords {
+    public static final Keywords DEFAULT_VALUE = factory().fromSet(ImmutableSet.of());
+
+    public interface KeywordsValidator {
+        void validate(Set<Keyword> keywords);
+    }
+
+    private FlagsBuilder combiner(FlagsBuilder firstBuilder, FlagsBuilder secondBuilder) {
+        return firstBuilder.add(secondBuilder.build());
+    }
+
+    private FlagsBuilder accumulator(FlagsBuilder accumulator, Keyword keyword) {
+        return accumulator.add(keyword.asFlags());
+    }
+
+    public static class KeywordsFactory {
+        private Optional<KeywordsValidator> validator;
+        private Optional<Predicate<Keyword>> filter;
+
+        private KeywordsFactory() {
+            validator = Optional.empty();
+            filter = Optional.empty();
+        }
+
+        public KeywordsFactory throwOnImapNonExposedKeywords() {
+            validator = Optional.of(keywords -> Preconditions.checkArgument(
+                keywords.stream().allMatch(Keyword::isExposedImapKeyword), "Does not allow to update 'Deleted' or 'Recent' flag"));
+            return this;
+        }
+
+        public KeywordsFactory filterImapNonExposedKeywords() {
+            filter = Optional.of(keyword -> keyword.isExposedImapKeyword());
+            return this;
+        }
+
+        public Keywords fromSet(Set<Keyword> setKeywords) {
+            validator.orElse(keywords -> {})
+                .validate(setKeywords);
+
+            return new Keywords(setKeywords.stream()
+                .filter(filter.orElse(keyword -> true))
+                .collect(Guavate.toImmutableSet()));
+        }
+
+        public Keywords fromList(List<String> keywords) {
+            return fromSet(keywords.stream()
+                .map(Keyword::new)
+                .collect(Guavate.toImmutableSet()));
+        }
+
+        @VisibleForTesting
+        Keywords fromMap(Map<String, Boolean> mapKeywords) {
+            Preconditions.checkArgument(mapKeywords.values()
+                .stream()
+                .noneMatch(keywordValue -> keywordValue == false), "Keyword must be true");
+            Set<Keyword> setKeywords = mapKeywords.keySet()
+                .stream()
+                .map(Keyword::new)
+                .collect(Guavate.toImmutableSet());
+
+            return fromSet(setKeywords);
+        }
+
+        @VisibleForTesting
+        Keywords fromOldKeyword(OldKeyword oldKeyword) {
+            ImmutableSet.Builder<Keyword> builder = ImmutableSet.builder();
+            if (oldKeyword.isAnswered().orElse(false)) {
+                builder.add(Keyword.ANSWERED);
+            }
+            if (oldKeyword.isDraft().orElse(false)) {
+                builder.add(Keyword.DRAFT);
+            }
+            if (oldKeyword.isFlagged().orElse(false)) {
+                builder.add(Keyword.FLAGGED);
+            }
+            if (oldKeyword.isUnread().isPresent() && oldKeyword.isUnread().get() == false) {
+                builder.add(Keyword.SEEN);
+            }
+            return fromSet(builder.build());
+        }
+
+        public Keywords fromFlags(Flags flags) {
+            return fromSet(Stream.concat(
+                    Stream.of(flags.getUserFlags())
+                        .map(Keyword::new),
+                    Stream.of(flags.getSystemFlags())
+                        .map(Keyword::fromFlag))
+                .collect(Guavate.toImmutableSet()));
+        }
+
+        public Optional<Keywords> fromMapOrOldKeyword(Optional<Map<String, Boolean>> mapKeyword, Optional<OldKeyword> oldKeyword) {
+            Preconditions.checkArgument(!(mapKeyword.isPresent() && oldKeyword.isPresent()), "Does not support keyword and is* at the same time");
+
+            Keywords keywords = mapKeyword.map(this::fromMap)
+                .orElse(oldKeyword.map(this::fromOldKeyword)
+                    .orElse(null));
+            return Optional.ofNullable(keywords);
+        }
+    }
+
+    public static KeywordsFactory factory() {
+        return new KeywordsFactory();
+    }
+
+    private final ImmutableSet<Keyword> keywords;
+
+    private Keywords(ImmutableSet<Keyword> keywords) {
+        this.keywords = keywords;
+    }
+
+    public Flags asFlags() {
+        return keywords.stream()
+            .reduce(FlagsBuilder.builder(), this::accumulator, this::combiner)
+            .build();
+    }
+
+    public Flags asFlagsWithRecentAndDeletedFrom(Flags originFlags) {
+        Flags flags = asFlags();
+        if (originFlags.contains(Flags.Flag.DELETED)) {
+            flags.add(Flags.Flag.DELETED);
+        }
+        if (originFlags.contains(Flags.Flag.RECENT)) {
+            flags.add(Flags.Flag.RECENT);
+        }
+
+        return flags;
+    }
+
+    public ImmutableMap<String, Boolean> asMap() {
+        return keywords.stream()
+            .collect(Guavate.toImmutableMap(Keyword::getFlagName, keyword -> Keyword.FLAG_VALUE));
+    }
+
+    public ImmutableSet<Keyword> getKeywords() {
+        return keywords;
+    }
+
+    public boolean contains(Keyword keyword) {
+        return keywords.contains(keyword);
+    }
+
+    @Override
+    public final boolean equals(Object other) {
+        if (other instanceof Keywords) {
+            Keywords otherKeyword = (Keywords) other;
+            return Objects.equal(keywords, otherKeyword.keywords);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hashCode(keywords);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("keywords", keywords)
+            .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/OldKeyword.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/OldKeyword.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/OldKeyword.java
new file mode 100644
index 0000000..e2cb2b3
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/OldKeyword.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * 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.model;
+
+import java.util.Optional;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+
+public class OldKeyword {
+    private final Optional<Boolean> isUnread;
+    private final Optional<Boolean> isFlagged;
+    private final Optional<Boolean> isAnswered;
+    private final Optional<Boolean> isDraft;
+
+    @VisibleForTesting
+    OldKeyword(boolean isUnread, boolean isFlagged, boolean isAnswered, boolean isDraft) {
+        this.isUnread = Optional.of(isUnread);
+        this.isFlagged = Optional.of(isFlagged);
+        this.isAnswered = Optional.of(isAnswered);
+        this.isDraft = Optional.of(isDraft);
+    }
+
+    public OldKeyword(Optional<Boolean> isUnread, Optional<Boolean> isFlagged, Optional<Boolean> isAnswered, Optional<Boolean> isDraft) {
+        this.isUnread = isUnread;
+        this.isFlagged = isFlagged;
+        this.isAnswered = isAnswered;
+        this.isDraft = isDraft;
+    }
+
+    public Optional<Boolean> isUnread() {
+        return isUnread;
+    }
+
+    public Optional<Boolean> isFlagged() {
+        return isFlagged;
+    }
+
+    public Optional<Boolean> isAnswered() {
+        return isAnswered;
+    }
+
+    public Optional<Boolean> isDraft() {
+        return isDraft;
+    }
+
+    @Override
+    public final boolean equals(Object other) {
+        if (other instanceof OldKeyword) {
+            OldKeyword oldKeyword = (OldKeyword) other;
+            return Objects.equal(isUnread, oldKeyword.isUnread)
+                && Objects.equal(isFlagged, oldKeyword.isFlagged)
+                && Objects.equal(isAnswered, oldKeyword.isAnswered)
+                && Objects.equal(isDraft, oldKeyword.isDraft);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hashCode(isUnread, isFlagged, isAnswered, isDraft);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("isUnread", isUnread)
+                .add("isFlagged", isFlagged)
+                .add("isAnswered", isAnswered)
+                .add("isDraft", isDraft)
+                .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordTest.java
new file mode 100644
index 0000000..12fcf0a
--- /dev/null
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordTest.java
@@ -0,0 +1,189 @@
+/****************************************************************
+ * 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.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Optional;
+import javax.mail.Flags;
+
+import org.apache.commons.lang3.StringUtils;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class KeywordTest {
+    private final static String FORWARDED = "forwarded";
+    private final static int FLAG_NAME_MAX_LENTH = 255;
+    private final static String ANY_KEYWORD = "AnyKeyword";
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void shouldRespectBeanContract() {
+        EqualsVerifier.forClass(Keyword.class).verify();
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameLengthLessThanMinLength() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameLengthMoreThanMaxLength() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword(StringUtils.repeat("a", FLAG_NAME_MAX_LENTH + 1));
+    }
+
+    @Test
+    public void keywordShouldCreateNewOneWhenFlagNameLengthEqualsMaxLength() throws Exception {
+        String maxLengthFlagName = StringUtils.repeat("a", FLAG_NAME_MAX_LENTH);
+        Keyword keyword = new Keyword(maxLengthFlagName);
+
+        assertThat(keyword.getFlagName()).isEqualTo(maxLengthFlagName);
+    }
+
+    @Test
+    public void keywordShouldCreateNewOneWhenFlagNameLengthEqualsMinLength() throws Exception {
+        String minLengthFlagName = "a";
+        Keyword keyword = new Keyword(minLengthFlagName);
+
+        assertThat(keyword.getFlagName()).isEqualTo(minLengthFlagName);
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsPercentageCharacter() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a%");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsLeftBracket() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a[");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsRightBracket() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a]");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsLeftBrace() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a{");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsSlash() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a\\");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsStar() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a*");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsQuote() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a\"");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsOpeningParenthesis() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a(");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsClosingParenthesis() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a)");
+    }
+
+    @Test
+    public void keywordShouldThrowWhenFlagNameContainsSpaceCharacter() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        new Keyword("a b");
+    }
+
+    @Test
+    public void isNotNonExposedImapKeywordShouldReturnFalseWhenDeleted() throws Exception {
+        assertThat(Keyword.DELETED.isExposedImapKeyword()).isFalse();
+    }
+
+    @Test
+    public void isNotNonExposedImapKeywordShouldReturnFalseWhenRecent() throws Exception {
+        assertThat(Keyword.RECENT.isExposedImapKeyword()).isFalse();
+    }
+
+    @Test
+    public void isNotNonExposedImapKeywordShouldReturnTrueWhenOtherSystemFlag() throws Exception {
+        assertThat(Keyword.DRAFT.isExposedImapKeyword()).isTrue();
+    }
+
+    @Test
+    public void isNotNonExposedImapKeywordShouldReturnTrueWhenAnyUserFlag() throws Exception {
+        Keyword keyword = new Keyword(ANY_KEYWORD);
+        assertThat(keyword.isExposedImapKeyword()).isTrue();
+    }
+
+    @Test
+    public void isDraftShouldReturnTrueWhenDraft() throws Exception {
+        assertThat(Keyword.DRAFT.isDraft()).isTrue();
+    }
+
+    @Test
+    public void isDraftShouldReturnFalseWhenNonDraft() throws Exception {
+        assertThat(Keyword.DELETED.isDraft()).isFalse();
+    }
+
+    @Test
+    public void asSystemFlagShouldReturnSystemFlag() throws Exception {
+        assertThat(new Keyword("$Draft").asSystemFlag())
+            .isEqualTo(Optional.of(Flags.Flag.DRAFT));
+    }
+
+    @Test
+    public void asSystemFlagShouldReturnEmptyWhenNonSystemFlag() throws Exception {
+        assertThat(new Keyword(ANY_KEYWORD).asSystemFlag().isPresent())
+            .isFalse();
+    }
+
+    @Test
+    public void asFlagsShouldReturnFlagsWhenSystemFlag() throws Exception {
+        assertThat(Keyword.DELETED.asFlags())
+            .isEqualTo(new Flags(Flags.Flag.DELETED));
+    }
+
+    @Test
+    public void asFlagsShouldReturnFlagsWhenUserFlag() throws Exception {
+        Keyword keyword = new Keyword(ANY_KEYWORD);
+        assertThat(keyword.asFlags())
+            .isEqualTo(new Flags(ANY_KEYWORD));
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordsTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordsTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordsTest.java
new file mode 100644
index 0000000..e992c59
--- /dev/null
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/KeywordsTest.java
@@ -0,0 +1,328 @@
+/****************************************************************
+ * 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.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Map;
+import java.util.Optional;
+import javax.mail.Flags;
+import javax.mail.Flags.Flag;
+
+import org.apache.james.mailbox.FlagsBuilder;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class KeywordsTest {
+    public static final String ANY_KEYWORD = "AnyKeyword";
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void shouldRespectBeanContract() {
+        EqualsVerifier.forClass(Keywords.class).verify();
+    }
+
+    @Test
+    public void fromMapShouldThrowWhenWrongKeywordValue() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+
+        Keywords.factory()
+            .fromMap(ImmutableMap.of(ANY_KEYWORD, false));
+    }
+
+    @Test
+    public void fromMapShouldReturnKeywordsFromMapStringAndBoolean() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .fromMap(ImmutableMap.of(ANY_KEYWORD, Keyword.FLAG_VALUE));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(new Keyword(ANY_KEYWORD));
+    }
+
+    @Test
+    public void fromFlagsShouldReturnKeywordsFromAllFlag() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .fromFlags(new Flags(Flags.Flag.ANSWERED));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED);
+    }
+
+    @Test
+    public void fromSetShouldReturnKeywordsFromSetOfKeywords() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .fromSet(ImmutableSet.of(Keyword.ANSWERED));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED);
+    }
+
+    @Test
+    public void fromOldKeywordsShouldReturnKeywordsFromIsAnswered() throws Exception {
+        Optional<Boolean> isAnswered = Optional.of(true);
+        Optional<Boolean> isUnread = Optional.empty();
+        Optional<Boolean> isFlagged = Optional.empty();
+        Optional<Boolean> isDraft = Optional.empty();
+        Keywords keywords = Keywords.factory()
+            .fromOldKeyword(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED);
+    }
+
+    @Test
+    public void fromOldKeywordsShouldReturnKeywordsFromIsDraft() throws Exception {
+        Optional<Boolean> isAnswered = Optional.empty();
+        Optional<Boolean> isUnread = Optional.empty();
+        Optional<Boolean> isFlagged = Optional.empty();
+        Optional<Boolean> isDraft = Optional.of(true);
+        Keywords keywords = Keywords.factory()
+            .fromOldKeyword(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.DRAFT);
+    }
+
+    @Test
+    public void fromOldKeywordsShouldReturnKeywordsFromIsFlagged() throws Exception {
+        Optional<Boolean> isAnswered = Optional.empty();
+        Optional<Boolean> isUnread = Optional.empty();
+        Optional<Boolean> isFlagged = Optional.of(true);
+        Optional<Boolean> isDraft = Optional.empty();
+        Keywords keywords = Keywords.factory()
+            .fromOldKeyword(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.FLAGGED);
+    }
+
+    @Test
+    public void fromOldKeywordsShouldReturnKeywordsFromIsUnread() throws Exception {
+        Optional<Boolean> isAnswered = Optional.empty();
+        Optional<Boolean> isUnread = Optional.of(false);
+        Optional<Boolean> isFlagged = Optional.empty();
+        Optional<Boolean> isDraft = Optional.empty();
+        Keywords keywords = Keywords.factory()
+            .fromOldKeyword(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.SEEN);
+    }
+
+    @Test
+    public void fromOldKeywordsShouldReturnKeywordsFromIsFlag() throws Exception {
+        Optional<Boolean> isAnswered = Optional.of(true);
+        Optional<Boolean> isUnread = Optional.of(true);
+        Optional<Boolean> isFlagged = Optional.of(true);
+        Optional<Boolean> isDraft = Optional.of(true);
+        Keywords keywords = Keywords.factory()
+            .fromOldKeyword(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED, Keyword.DRAFT, Keyword.FLAGGED);
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldReturnEmptyWhenBothAreEmpty() throws Exception {
+        assertThat(Keywords.factory()
+            .fromMapOrOldKeyword(Optional.empty(), Optional.empty()))
+            .isEmpty();
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldReturnKeywordsWhenMap() throws Exception {
+        assertThat(Keywords.factory()
+            .fromMapOrOldKeyword(Optional.of(ImmutableMap.of()), Optional.empty()))
+            .isPresent();
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldReturnKeywordsWhenOldKeyword() throws Exception {
+        Optional<Boolean> isAnswered = Optional.of(true);
+        Optional<Boolean> isUnread = Optional.of(true);
+        Optional<Boolean> isFlagged = Optional.of(true);
+        Optional<Boolean> isDraft = Optional.of(true);
+
+        OldKeyword oldKeyword = new OldKeyword(isUnread, isFlagged, isAnswered, isDraft);
+
+        assertThat(Keywords.factory()
+            .fromMapOrOldKeyword(Optional.empty(), Optional.of(oldKeyword)))
+            .isPresent();
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldThrownWhenBothMapAndOldKeyword() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+        Optional<Boolean> isAnswered = Optional.of(true);
+        Optional<Boolean> isUnread = Optional.of(true);
+        Optional<Boolean> isFlagged = Optional.of(true);
+        Optional<Boolean> isDraft = Optional.of(true);
+
+        OldKeyword oldKeyword = new OldKeyword(isUnread, isFlagged, isAnswered, isDraft);
+
+        Keywords.factory()
+            .fromMapOrOldKeyword(Optional.of(ImmutableMap.of()), Optional.of(oldKeyword));
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldReturnKeywordsFromMap() throws Exception {
+        ImmutableMap<String, Boolean> mapKeywords = ImmutableMap.of(ANY_KEYWORD, Keyword.FLAG_VALUE);
+
+        assertThat(Keywords.factory()
+            .fromMapOrOldKeyword(Optional.of(mapKeywords), Optional.empty())
+            .get()
+            .getKeywords())
+            .containsOnly(new Keyword(ANY_KEYWORD));
+    }
+
+    @Test
+    public void fromMapOrOldKeywordShouldReturnKeywordsFromOldKeyword() throws Exception {
+        Optional<Boolean> isAnswered = Optional.of(true);
+        Optional<Boolean> isUnread = Optional.of(true);
+        Optional<Boolean> isFlagged = Optional.of(true);
+        Optional<Boolean> isDraft = Optional.of(true);
+
+        OldKeyword oldKeyword = new OldKeyword(isUnread, isFlagged, isAnswered, isDraft);
+
+        Keywords keywords = Keywords.factory()
+            .fromMapOrOldKeyword(Optional.empty(), Optional.of(oldKeyword))
+            .get();
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED, Keyword.DRAFT, Keyword.FLAGGED);
+    }
+
+    @Test
+    public void asFlagsShouldBuildFlagsFromKeywords() throws Exception {
+        assertThat(Keywords.factory()
+                .fromSet(ImmutableSet.of(Keyword.ANSWERED))
+                .asFlags())
+            .isEqualTo(new Flags(Flags.Flag.ANSWERED));
+    }
+
+    @Test
+    public void asFlagsWithRecentAndDeletedFromShouldBuildFlagsFromKeywordsAndRecentOriginFlags() throws Exception {
+        Flags originFlags = FlagsBuilder.builder()
+            .add(Flag.RECENT, Flag.DRAFT)
+            .build();
+
+        Flags expectedFlags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.RECENT)
+            .build();
+
+        assertThat(Keywords.factory()
+                .fromSet(ImmutableSet.of(Keyword.ANSWERED))
+                .asFlagsWithRecentAndDeletedFrom(originFlags))
+            .isEqualTo(expectedFlags);
+    }
+
+    @Test
+    public void asFlagsWithRecentAndDeletedFromShouldBuildFlagsFromKeywordsWithDeletedAndRecentOriginFlags() throws Exception {
+        Flags originFlags = FlagsBuilder.builder()
+            .add(Flag.RECENT, Flag.DELETED, Flag.DRAFT)
+            .build();
+
+        Flags expectedFlags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.RECENT, Flag.DELETED)
+            .build();
+
+        assertThat(Keywords.factory()
+                .fromSet(ImmutableSet.of(Keyword.ANSWERED))
+                .asFlagsWithRecentAndDeletedFrom(originFlags))
+            .isEqualTo(expectedFlags);
+    }
+
+    @Test
+    public void asMapShouldReturnEmptyWhenEmptyMapOfStringAndBoolean() throws Exception {
+        assertThat(Keywords.factory()
+                .fromSet(ImmutableSet.of())
+                .asMap())
+            .isEmpty();;
+    }
+
+    @Test
+    public void asMapShouldReturnMapOfStringAndBoolean() throws Exception {
+        Map<String, Boolean> expectedMap = ImmutableMap.of("$Answered", Keyword.FLAG_VALUE);
+        assertThat(Keywords.factory()
+                .fromSet(ImmutableSet.of(Keyword.ANSWERED))
+                .asMap())
+            .isEqualTo(expectedMap);
+    }
+
+    @Test
+    public void throwWhenUnsupportedKeywordShouldThrowWhenHaveUnsupportedKeywords() throws Exception {
+        expectedException.expect(IllegalArgumentException.class);
+
+        Keywords.factory()
+            .throwOnImapNonExposedKeywords()
+            .fromSet(ImmutableSet.of(Keyword.DRAFT, Keyword.DELETED));
+    }
+
+    @Test
+    public void throwWhenUnsupportedKeywordShouldNotThrowWhenHaveDraft() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .throwOnImapNonExposedKeywords()
+            .fromSet(ImmutableSet.of(Keyword.ANSWERED, Keyword.DRAFT));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED, Keyword.DRAFT);
+    }
+
+    @Test
+    public void filterUnsupportedShouldFilter() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .filterImapNonExposedKeywords()
+            .fromSet(ImmutableSet.of(Keyword.ANSWERED, Keyword.DELETED, Keyword.RECENT, Keyword.DRAFT));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED, Keyword.DRAFT);
+    }
+
+    @Test
+    public void containsShouldReturnTrueWhenKeywordsContainKeyword() {
+        Keywords keywords = Keywords.factory()
+            .fromSet(ImmutableSet.of(Keyword.SEEN));
+
+        assertThat(keywords.contains(Keyword.SEEN)).isTrue();
+    }
+
+    @Test
+    public void containsShouldReturnFalseWhenKeywordsDoNotContainKeyword() {
+        Keywords keywords = Keywords.factory()
+            .fromSet(ImmutableSet.of());
+
+        assertThat(keywords.contains(Keyword.SEEN)).isFalse();
+    }
+
+    @Test
+    public void fromListShouldReturnKeywordsFromListOfStrings() throws Exception {
+        Keywords keywords = Keywords.factory()
+            .fromList(ImmutableList.of("$Answered", "$Flagged"));
+
+        assertThat(keywords.getKeywords())
+            .containsOnly(Keyword.ANSWERED, Keyword.FLAGGED);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/20beee7a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/OldKeywordTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/OldKeywordTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/OldKeywordTest.java
new file mode 100644
index 0000000..024271a
--- /dev/null
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/OldKeywordTest.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * 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.model;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.Test;
+
+public class OldKeywordTest {
+    @Test
+    public void shouldRespectBeanContract() {
+        EqualsVerifier.forClass(OldKeyword.class).verify();
+    }
+
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[02/13] james-project git commit: JAMES-2110 GetMessageList should allow filtering by keywords

Posted by bt...@apache.org.
JAMES-2110 GetMessageList should allow filtering by keywords


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

Branch: refs/heads/master
Commit: a6f1c1059d20dd179e9f5feb2e3374c38869cfed
Parents: 92365a2
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:45:26 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:27 2017 +0200

----------------------------------------------------------------------
 .../james/jmap/model/FilterCondition.java       | 48 ++++++++++--
 .../james/jmap/utils/FilterToSearchQuery.java   | 27 ++++++-
 .../james/jmap/model/FilterConditionTest.java   | 77 +++++++++++++++++++-
 .../jmap/utils/FilterToSearchQueryTest.java     | 52 +++++++++++++
 4 files changed, 196 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java
index 63663c5..334667d 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 @JsonDeserialize(builder = FilterCondition.Builder.class)
@@ -60,10 +61,14 @@ public class FilterCondition implements Filter {
         private String subject;
         private String body;
         private Header header;
+        private Optional<String> hasKeyword;
+        private Optional<String> notKeyword;
 
         private Builder() {
             inMailboxes = Optional.empty();
             notInMailboxes = Optional.empty();
+            hasKeyword = Optional.empty();
+            notKeyword = Optional.empty();
         }
 
         public Builder inMailboxes(String... inMailboxes) {
@@ -88,6 +93,18 @@ public class FilterCondition implements Filter {
             return this;
         }
 
+        @JsonDeserialize
+        public Builder hasKeyword(Optional<String> hasKeyword) {
+            this.hasKeyword = hasKeyword;
+            return this;
+        }
+
+        @JsonDeserialize
+        public Builder notKeyword(Optional<String> notKeyword) {
+            this.notKeyword = notKeyword;
+            return this;
+        }
+
         public Builder before(ZonedDateTime before) {
             this.before = before;
             return this;
@@ -174,9 +191,12 @@ public class FilterCondition implements Filter {
         }
 
         public FilterCondition build() {
+            Preconditions.checkArgument(!hasKeyword.isPresent() || (new Keyword(hasKeyword.get()) != null), "hasKeyword is not valid");
+            Preconditions.checkArgument(!notKeyword.isPresent() || (new Keyword(notKeyword.get()) != null), "notKeyword is not valid");
             return new FilterCondition(inMailboxes, notInMailboxes, Optional.ofNullable(before), Optional.ofNullable(after), Optional.ofNullable(minSize), Optional.ofNullable(maxSize),
                     Optional.ofNullable(isFlagged), Optional.ofNullable(isUnread), Optional.ofNullable(isAnswered), Optional.ofNullable(isDraft), Optional.ofNullable(hasAttachment),
-                    Optional.ofNullable(text), Optional.ofNullable(from), Optional.ofNullable(to), Optional.ofNullable(cc), Optional.ofNullable(bcc), Optional.ofNullable(subject), Optional.ofNullable(body), Optional.ofNullable(header));
+                    Optional.ofNullable(text), Optional.ofNullable(from), Optional.ofNullable(to), Optional.ofNullable(cc), Optional.ofNullable(bcc), Optional.ofNullable(subject),
+                    Optional.ofNullable(body), Optional.ofNullable(header), hasKeyword, notKeyword);
         }
     }
 
@@ -199,10 +219,13 @@ public class FilterCondition implements Filter {
     private final Optional<String> subject;
     private final Optional<String> body;
     private final Optional<Header> header;
+    private final Optional<String> hasKeyword;
+    private final Optional<String> notKeyword;
 
     @VisibleForTesting FilterCondition(Optional<List<String>> inMailboxes, Optional<List<String>> notInMailboxes, Optional<ZonedDateTime> before, Optional<ZonedDateTime> after, Optional<Integer> minSize, Optional<Integer> maxSize,
-            Optional<Boolean> isFlagged, Optional<Boolean> isUnread, Optional<Boolean> isAnswered, Optional<Boolean> isDraft, Optional<Boolean> hasAttachment,
-            Optional<String> text, Optional<String> from, Optional<String> to, Optional<String> cc, Optional<String> bcc, Optional<String> subject, Optional<String> body, Optional<Header> header) {
+                                       Optional<Boolean> isFlagged, Optional<Boolean> isUnread, Optional<Boolean> isAnswered, Optional<Boolean> isDraft, Optional<Boolean> hasAttachment,
+                                       Optional<String> text, Optional<String> from, Optional<String> to, Optional<String> cc, Optional<String> bcc, Optional<String> subject,
+                                       Optional<String> body, Optional<Header> header, Optional<String> hasKeyword, Optional<String> notKeyword) {
 
         this.inMailboxes = inMailboxes;
         this.notInMailboxes = notInMailboxes;
@@ -223,6 +246,8 @@ public class FilterCondition implements Filter {
         this.subject = subject;
         this.body = body;
         this.header = header;
+        this.hasKeyword = hasKeyword;
+        this.notKeyword = notKeyword;
     }
 
     public Optional<List<String>> getInMailboxes() {
@@ -301,6 +326,14 @@ public class FilterCondition implements Filter {
         return header;
     }
 
+    public Optional<String> getHasKeyword() {
+        return hasKeyword;
+    }
+
+    public Optional<String> getNotKeyword() {
+        return notKeyword;
+    }
+
     @Override
     public final boolean equals(Object obj) {
         if (obj instanceof FilterCondition) {
@@ -323,14 +356,17 @@ public class FilterCondition implements Filter {
                 && Objects.equals(this.bcc, other.bcc)
                 && Objects.equals(this.subject, other.subject)
                 && Objects.equals(this.body, other.body)
-                && Objects.equals(this.header, other.header);
+                && Objects.equals(this.header, other.header)
+                && Objects.equals(this.hasKeyword, other.hasKeyword)
+                && Objects.equals(this.notKeyword, other.notKeyword);
         }
         return false;
     }
 
     @Override
     public final int hashCode() {
-        return Objects.hash(inMailboxes, notInMailboxes, before, after, minSize, maxSize, isFlagged, isUnread, isAnswered, isDraft, hasAttachment, text, from, to, cc, bcc, subject, body, header);
+        return Objects.hash(inMailboxes, notInMailboxes, before, after, minSize, maxSize, isFlagged, isUnread, isAnswered, isDraft, hasAttachment,
+                text, from, to, cc, bcc, subject, body, header, hasKeyword, notKeyword);
     }
 
     @Override
@@ -355,6 +391,8 @@ public class FilterCondition implements Filter {
         subject.ifPresent(x -> helper.add("subject", x));
         body.ifPresent(x -> helper.add("body", x));
         header.ifPresent(x -> helper.add("header", x));
+        hasKeyword.ifPresent(x -> helper.add("hasKeyword", x));
+        notKeyword.ifPresent(x -> helper.add("notKeyword", x));
         return helper.toString();
     }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java
index 95799e4..f619357 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java
@@ -20,12 +20,13 @@
 package org.apache.james.jmap.utils;
 
 import java.util.Date;
-
+import java.util.Optional;
 import javax.mail.Flags.Flag;
 
 import org.apache.james.jmap.model.Filter;
 import org.apache.james.jmap.model.FilterCondition;
 import org.apache.james.jmap.model.FilterOperator;
+import org.apache.james.jmap.model.Keyword;
 import org.apache.james.mailbox.model.SearchQuery;
 import org.apache.james.mailbox.model.SearchQuery.AddressType;
 import org.apache.james.mailbox.model.SearchQuery.Criterion;
@@ -75,9 +76,33 @@ public class FilterToSearchQuery {
         filter.getMaxSize().ifPresent(maxSize -> searchQuery.andCriteria(SearchQuery.sizeLessThan(maxSize)));
         filter.getMinSize().ifPresent(minSize -> searchQuery.andCriteria(SearchQuery.sizeGreaterThan(minSize)));
         filter.getHasAttachment().ifPresent(hasAttachment -> searchQuery.andCriteria(SearchQuery.hasAttachment(hasAttachment)));
+        filter.getHasKeyword().ifPresent(hasKeyword -> {
+            keywordQuery(hasKeyword, true).ifPresent(hasKeywordCriterion
+                -> searchQuery.andCriteria(hasKeywordCriterion));
+        });
+        filter.getNotKeyword().ifPresent(notKeyword -> {
+            keywordQuery(notKeyword, false).ifPresent(notKeywordCriterion
+                -> searchQuery.andCriteria(notKeywordCriterion));
+        });
+
         return searchQuery;
     }
 
+    private Optional<Criterion> keywordQuery(String stringKeyword, boolean isSet) {
+        Keyword keyword = new Keyword(stringKeyword);
+        if (keyword.isExposedImapKeyword()) {
+            return Optional.of(getFlagCriterion(keyword, isSet));
+        }
+
+        return Optional.empty();
+    }
+
+    private Criterion getFlagCriterion(Keyword keyword, boolean isSet) {
+        return keyword.asSystemFlag()
+            .map(flag -> SearchQuery.flagSet(flag, isSet))
+            .orElse(SearchQuery.flagSet(keyword.getFlagName(), isSet));
+    }
+
     private Criterion convertOperator(FilterOperator filter) {
         switch (filter.getOperator()) {
         case AND:

http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java
index 5a518f0..5efaecc 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java
@@ -23,15 +23,22 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import java.time.ZonedDateTime;
 import java.util.Optional;
+import java.util.Set;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.rules.ExpectedException;
 
 public class FilterConditionTest {
 
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
     @Test
     public void buildShouldWorkWhenNoInMailboxes() {
         FilterCondition filterCondition = FilterCondition.builder().build();
@@ -89,9 +96,13 @@ public class FilterConditionTest {
         String subject = "subject";
         String body = "body";
         Header header = Header.from(ImmutableList.of("name", "value"));
-        FilterCondition expectedFilterCondition = new FilterCondition(Optional.of(ImmutableList.of("1")), Optional.of(ImmutableList.of("2")), Optional.of(before), Optional.of(after), Optional.of(minSize), Optional.of(maxSize), 
+        Optional<String> hasKeyword = Optional.of("$Draft");
+        Optional<String> notKeyword = Optional.of("$Flagged");
+
+        FilterCondition expectedFilterCondition = new FilterCondition(Optional.of(ImmutableList.of("1")), Optional.of(ImmutableList.of("2")), Optional.of(before), Optional.of(after), Optional.of(minSize), Optional.of(maxSize),
                 Optional.of(isFlagged), Optional.of(isUnread), Optional.of(isAnswered), Optional.of(isDraft), Optional.of(hasAttachment), Optional.of(text), Optional.of(from), 
-                Optional.of(to), Optional.of(cc), Optional.of(bcc), Optional.of(subject), Optional.of(body), Optional.of(header));
+                Optional.of(to), Optional.of(cc), Optional.of(bcc), Optional.of(subject), Optional.of(body), Optional.of(header),
+                hasKeyword, notKeyword);
 
         FilterCondition filterCondition = FilterCondition.builder()
                 .inMailboxes(Optional.of(ImmutableList.of("1")))
@@ -113,6 +124,8 @@ public class FilterConditionTest {
                 .subject(subject)
                 .body(body)
                 .header(header)
+                .hasKeyword(hasKeyword)
+                .notKeyword(notKeyword)
                 .build();
 
         assertThat(filterCondition).isEqualToComparingFieldByField(expectedFilterCondition);
@@ -122,4 +135,64 @@ public class FilterConditionTest {
     public void shouldRespectJavaBeanContract() {
         EqualsVerifier.forClass(FilterCondition.class).verify();
     }
+
+    @Test
+    public void buildShouldBuildFilterConditionWithHasKeywordWhenGivenHasKeyword() {
+        String hasKeyword = "$Draft";
+
+        FilterCondition filterCondition = FilterCondition.builder()
+            .hasKeyword(Optional.of(hasKeyword))
+            .build();
+
+        assertThat(filterCondition.getHasKeyword().get())
+            .isEqualTo(hasKeyword);
+    }
+
+    @Test
+    public void buildShouldBuildFilterConditionWithoutHasKeywordWhenDoNotGivenHasKeyword() {
+        FilterCondition filterCondition = FilterCondition.builder()
+            .hasKeyword(Optional.empty())
+            .build();
+
+        assertThat(filterCondition.getHasKeyword().isPresent())
+            .isFalse();
+    }
+
+    @Test
+    public void buildShouldThrowWhenGivenInvalidKeywordAsHasKeyword() {
+        expectedException.expect(IllegalArgumentException.class);
+
+        FilterCondition.builder()
+            .hasKeyword(Optional.of("$Draft%"))
+            .build();
+    }
+
+    @Test
+    public void buildShouldBuildFilterConditionWithNotKeywordWhenGivenNotKeyword() {
+        String notKeyword = "$Draft";
+
+        FilterCondition filterCondition = FilterCondition.builder()
+            .notKeyword(Optional.of(notKeyword))
+            .build();
+        assertThat(filterCondition.getNotKeyword().get()).isEqualTo(notKeyword);
+    }
+
+    @Test
+    public void buildShouldBuildFilterConditionWithoutNotKeywordWhenDoNotGivenNotKeyword() {
+        FilterCondition filterCondition = FilterCondition.builder()
+            .notKeyword(Optional.empty())
+            .build();
+
+        assertThat(filterCondition.getNotKeyword().isPresent())
+            .isFalse();
+    }
+
+    @Test
+    public void buildShouldThrowWhenGivenInvalidKeywordAsNotKeyword() {
+        expectedException.expect(IllegalArgumentException.class);
+
+        FilterCondition.builder()
+            .notKeyword(Optional.of("$Draft%"))
+            .build();
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java
index eb05436..aca9757 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.time.ZonedDateTime;
 import java.util.Date;
+import java.util.Optional;
 
 import javax.mail.Flags.Flag;
 
@@ -31,14 +32,17 @@ import org.apache.james.jmap.model.Filter;
 import org.apache.james.jmap.model.FilterCondition;
 import org.apache.james.jmap.model.FilterOperator;
 import org.apache.james.jmap.model.Header;
+import org.apache.james.jmap.model.Keyword;
 import org.apache.james.mailbox.model.SearchQuery;
 import org.apache.james.mailbox.model.SearchQuery.AddressType;
 import org.apache.james.mailbox.model.SearchQuery.DateResolution;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 public class FilterToSearchQueryTest {
+    private final static String FORWARDED = "forwarded";
 
     @Test
     public void filterConditionShouldThrowWhenUnknownFilter() {
@@ -476,4 +480,52 @@ public class FilterToSearchQueryTest {
 
         assertThat(searchQuery).isEqualTo(expectedSearchQuery);
     }
+
+    @Test
+    public void filterConditionShouldMapWhenHasKeyword() {
+        SearchQuery expectedSearchQuery = new SearchQuery();
+        expectedSearchQuery.andCriteria(SearchQuery.flagIsSet(Flag.FLAGGED));
+
+        SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder()
+                .hasKeyword(Optional.of("$Flagged"))
+                .build());
+
+        assertThat(searchQuery).isEqualTo(expectedSearchQuery);
+    }
+
+    @Test
+    public void filterConditionShouldMapWhenHasKeywordWithUserFlag() {
+        SearchQuery expectedSearchQuery = new SearchQuery();
+        expectedSearchQuery.andCriteria(SearchQuery.flagIsSet(FORWARDED));
+
+        SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder()
+                .hasKeyword(Optional.of(FORWARDED))
+                .build());
+
+        assertThat(searchQuery).isEqualTo(expectedSearchQuery);
+    }
+
+    @Test
+    public void filterConditionShouldMapWhenNotKeyword() {
+        SearchQuery expectedSearchQuery = new SearchQuery();
+        expectedSearchQuery.andCriteria(SearchQuery.flagIsUnSet(Flag.FLAGGED));
+
+        SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder()
+            .notKeyword(Optional.of("$Flagged"))
+            .build());
+
+        assertThat(searchQuery).isEqualTo(expectedSearchQuery);
+    }
+
+    @Test
+    public void filterConditionShouldMapWhenNotKeywordWithUserFlag() {
+        SearchQuery expectedSearchQuery = new SearchQuery();
+        expectedSearchQuery.andCriteria(SearchQuery.flagIsUnSet(FORWARDED));
+
+        SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder()
+                .notKeyword(Optional.of(FORWARDED))
+                .build());
+
+        assertThat(searchQuery).isEqualTo(expectedSearchQuery);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[08/13] james-project git commit: JAMES-2110 IT for getMessages which return keywords of imap flags

Posted by bt...@apache.org.
JAMES-2110 IT for getMessages which return keywords of imap flags


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

Branch: refs/heads/master
Commit: 41e3d15d299a7d86dcc70a47eaab69f0d2c9c58c
Parents: 37cf8ff
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:48:11 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:28 2017 +0200

----------------------------------------------------------------------
 .../cucumber/GetMessagesMethodStepdefs.java     |  38 +++++-
 .../test/resources/cucumber/GetMessages.feature |  33 +++++
 .../jmap/methods/GetMessagesMethodTest.java     | 135 ++++++++++++++++++-
 3 files changed, 193 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/41e3d15d/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/GetMessagesMethodStepdefs.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/GetMessagesMethodStepdefs.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/GetMessagesMethodStepdefs.java
index 13200f9..f42a071 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/GetMessagesMethodStepdefs.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/GetMessagesMethodStepdefs.java
@@ -42,6 +42,7 @@ import org.apache.http.HttpResponse;
 import org.apache.http.client.fluent.Request;
 import org.apache.james.jmap.DefaultMailboxes;
 import org.apache.james.jmap.methods.integration.cucumber.util.TableRow;
+import org.apache.james.jmap.model.Keywords;
 import org.apache.james.jmap.model.MessagePreviewGenerator;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxId;
@@ -84,11 +85,11 @@ public class GetMessagesMethodStepdefs {
     private final MainStepdefs mainStepdefs;
     private final UserStepdefs userStepdefs;
     private final Map<String, MessageId> messageIdsByName;
-    
+
     private HttpResponse response;
     private DocumentContext jsonPath;
     private List<MessageId> requestedMessageIds;
-    
+
     @Inject
     private GetMessagesMethodStepdefs(MainStepdefs mainStepdefs, UserStepdefs userStepdefs) {
         this.mainStepdefs = mainStepdefs;
@@ -147,9 +148,9 @@ public class GetMessagesMethodStepdefs {
 
     private MessageId appendMessage(String mailbox, ContentType contentType, String subject, String content, Optional<Map<String, String>> headers) throws Exception {
         ZonedDateTime dateTime = ZonedDateTime.parse("2014-10-30T14:12:00Z");
-        return mainStepdefs.jmapServer.getProbe(MailboxProbeImpl.class).appendMessage(userStepdefs.lastConnectedUser, 
+        return mainStepdefs.jmapServer.getProbe(MailboxProbeImpl.class).appendMessage(userStepdefs.lastConnectedUser,
                 new MailboxPath(MailboxConstants.USER_NAMESPACE, userStepdefs.lastConnectedUser, mailbox),
-                new ByteArrayInputStream(message(contentType, subject, content, headers).getBytes(Charsets.UTF_8)), 
+                new ByteArrayInputStream(message(contentType, subject, content, headers).getBytes(Charsets.UTF_8)),
                 Date.from(dateTime.toInstant()), false, new Flags()).getMessageId();
     }
 
@@ -246,6 +247,25 @@ public class GetMessagesMethodStepdefs {
         appendMessage(messageName, "eml/htmlWithLongAndComplicatedContent.eml");
     }
 
+    @Given("^the user has a message \"([^\"]*)\" in the \"([^\"]*)\" mailbox with flags \"([^\"]*)\"$")
+    public void appendMessageWithFlags(String messageName, String mailbox, List<String> keywords) throws Exception {
+        Flags flags = Keywords.factory()
+            .fromList(keywords)
+            .asFlags();
+        appendMessage(messageName, flags);
+    }
+
+    private void appendMessage(String messageName, Flags flags) throws Exception {
+        ZonedDateTime dateTime = ZonedDateTime.parse("2014-10-30T14:12:00Z");
+        boolean isRecent = flags.contains(Flags.Flag.RECENT);
+        MessageId id = mainStepdefs.jmapServer.getProbe(MailboxProbeImpl.class).appendMessage(userStepdefs.lastConnectedUser,
+                new MailboxPath(MailboxConstants.USER_NAMESPACE, userStepdefs.lastConnectedUser, DefaultMailboxes.INBOX),
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()),
+                Date.from(dateTime.toInstant()), isRecent, flags)
+                .getMessageId();
+        messageIdsByName.put(messageName, id);
+    }
+
     private void appendMessage(String messageName, String emlFileName) throws Exception {
         ZonedDateTime dateTime = ZonedDateTime.parse("2014-10-30T14:12:00Z");
         MessageId id = mainStepdefs.jmapServer.getProbe(MailboxProbeImpl.class).appendMessage(userStepdefs.lastConnectedUser,
@@ -377,12 +397,12 @@ public class GetMessagesMethodStepdefs {
         assertThat(jsonPath.<List<String>>read(ARGUMENTS + ".notFound")).containsExactlyElementsOf(elements);
     }
 
-    
+
     @Then("^the list should contain (\\d+) message$")
     public void assertListContains(int numberOfMessages) throws Exception {
         assertThat(jsonPath.<List<String>>read(ARGUMENTS + ".list")).hasSize(numberOfMessages);
     }
-    
+
     @Then("^the id of the message is \"([^\"]*)\"$")
     public void assertIdOfTheFirstMessage(String messageName) throws Exception {
         MessageId id = messageIdsByName.get(messageName);
@@ -503,6 +523,12 @@ public class GetMessagesMethodStepdefs {
         assertThat(actual).contains(preview);
     }
 
+    @Then("^the keywords of the message is (.*)$")
+    public void assertKeywordsOfMessageShouldDisplay(List<String> keywords) throws Exception {
+        assertThat(jsonPath.<Map<String, Boolean>>read(FIRST_MESSAGE + ".keywords").keySet())
+            .containsOnlyElementsOf(keywords);
+    }
+
     private void assertAttachment(String attachment, DataTable attachmentProperties) {
         attachmentProperties.asList(TableRow.class)
             .forEach(entry -> assertThat(jsonPath.<Object>read(attachment + "." + entry.getKey())).isEqualTo(entry.getValue()));

http://git-wip-us.apache.org/repos/asf/james-project/blob/41e3d15d/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
index 9f0255f..78addb9 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
@@ -318,3 +318,36 @@ Feature: GetMessages method
     Examples:
             |content-type                                       |tranfer-encoding   |content                                                                                                     |preview                                                                                      |
             |"text/html; charset=iso-8859-1"                    |quoted-printable   |"Dans le cadre du stage effectu=E9 Mlle 2017, =E0 sign=E9e d=E8s que possible, =E0, tr=E8s, journ=E9e.."    |effectué, à, signée dès, très, journée                                                                        |
+
+  Scenario Outline: Retrieving message should display keywords as jmap flag
+    Given the user has a message "m1" in the "inbox" mailbox with flags <flags>
+    When the user ask for messages "m1"
+    Then no error is returned
+    And the list should contain 1 message
+    And the keywords of the message is <keyword>
+
+    Examples:
+            |flags                          |keyword                        |
+            |"$Flagged,$Answered,$Draft"    |$Flagged,$Answered,$Draft      |
+
+  Scenario Outline: Retrieving message should display keywords without unsupported jmap flag
+    Given the user has a message "m1" in the "inbox" mailbox with flags <flags>
+    When the user ask for messages "m1"
+    Then no error is returned
+    And the list should contain 1 message
+    And the keywords of the message is <keyword>
+
+    Examples:
+            |flags                                  |keyword                 |
+            |"$Flagged,$Answered,$Deleted,$Recent"  |$Flagged,$Answered      |
+
+  Scenario Outline: Retrieving message should display keywords with custom user jmap flag
+    Given the user has a message "m1" in the "inbox" mailbox with flags <flags>
+    When the user ask for messages "m1"
+    Then no error is returned
+    And the list should contain 1 message
+    And the keywords of the message is <keyword>
+
+    Examples:
+            |flags                    |keyword                 |
+            |"$Flagged,$Forwarded"    |$Forwarded,$Flagged     |

http://git-wip-us.apache.org/repos/asf/james-project/blob/41e3d15d/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java
index c403251..6d7b06a 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/GetMessagesMethodTest.java
@@ -23,7 +23,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
-
 import java.io.ByteArrayInputStream;
 import java.util.Date;
 import java.util.List;
@@ -34,6 +33,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.mail.Flags;
+import javax.mail.Flags.Flag;
 
 import com.google.common.collect.ImmutableSet;
 import org.apache.commons.lang.NotImplementedException;
@@ -46,6 +46,7 @@ import org.apache.james.jmap.model.MessagePreviewGenerator;
 import org.apache.james.jmap.model.MessageProperties.MessageProperty;
 import org.apache.james.jmap.utils.HtmlTextExtractor;
 import org.apache.james.jmap.utils.JsoupHtmlTextExtractor;
+import org.apache.james.mailbox.FlagsBuilder;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageIdManager;
@@ -60,11 +61,6 @@ import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.logger.DefaultMetricFactory;
 import org.apache.james.util.mime.MessageContentExtractor;
-import org.assertj.core.api.Condition;
-import org.assertj.core.data.MapEntry;
-import org.assertj.core.groups.Tuple;
-import org.junit.Before;
-import org.junit.Test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
@@ -72,11 +68,18 @@ import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.jayway.jsonpath.JsonPath;
 
-public class GetMessagesMethodTest {
+import org.assertj.core.api.Condition;
+import org.assertj.core.data.MapEntry;
+import org.assertj.core.groups.Tuple;
+import org.junit.Before;
+import org.junit.Test;
 
+public class GetMessagesMethodTest {
+    private final static String FORWARDED = "forwarded";
     public static final Flags FLAGS = null;
     public static final boolean NOT_RECENT = false;
     private MessageIdManager messageIdManager;
@@ -495,4 +498,122 @@ public class GetMessagesMethodTest {
         GetMessagesResponse getMessagesResponse = (GetMessagesResponse) response;
         assertThat(getMessagesResponse.list()).hasSize(1);
     }
+
+    @Test
+    public void processShouldReturnKeywordsForMessageFlags() throws MailboxException {
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DRAFT)
+            .build();
+        MessageManager inbox = mailboxManager.getMailbox(inboxPath, session);
+        Date now = new Date();
+        ByteArrayInputStream message1Content = new ByteArrayInputStream("Subject: message 1 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message1 = inbox.appendMessage(message1Content, now, session, false, flags);
+        ByteArrayInputStream message2Content = new ByteArrayInputStream("Subject: message 2 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message2 = inbox.appendMessage(message2Content, now, session, false, flags);
+        ByteArrayInputStream message3Content = new ByteArrayInputStream("Great-Header: message 3 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message3 = inbox.appendMessage(message3Content, now, session, false, flags);
+
+        GetMessagesRequest request = GetMessagesRequest.builder()
+            .ids(ImmutableList.of(message1.getMessageId(),
+                message2.getMessageId(),
+                message3.getMessageId()))
+            .build();
+
+        List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList());
+
+        assertThat(result).hasSize(1)
+            .extracting(JmapResponse::getResponse)
+            .hasOnlyElementsOfType(GetMessagesResponse.class)
+            .extracting(GetMessagesResponse.class::cast)
+            .flatExtracting(GetMessagesResponse::list)
+            .extracting(Message::getKeywords)
+            .containsOnly(
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true),
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true),
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true));
+
+    }
+
+
+    @Test
+    public void processShouldReturnKeywordsWithoutUnsupportedKeywordsForMessageFlags() throws MailboxException {
+        Flags flags1 = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DRAFT, Flag.DELETED)
+            .build();
+        Flags flags2 = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DRAFT)
+            .build();
+        Flags flags3 = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DRAFT, Flag.RECENT)
+            .build();
+        MessageManager inbox = mailboxManager.getMailbox(inboxPath, session);
+        Date now = new Date();
+        ByteArrayInputStream message1Content = new ByteArrayInputStream("Subject: message 1 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message1 = inbox.appendMessage(message1Content, now, session, false, flags1);
+        ByteArrayInputStream message2Content = new ByteArrayInputStream("Subject: message 2 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message2 = inbox.appendMessage(message2Content, now, session, false, flags2);
+        ByteArrayInputStream message3Content = new ByteArrayInputStream("Great-Header: message 3 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message3 = inbox.appendMessage(message3Content, now, session, false, flags3);
+
+        GetMessagesRequest request = GetMessagesRequest.builder()
+            .ids(ImmutableList.of(message1.getMessageId(),
+                message2.getMessageId(),
+                message3.getMessageId()))
+            .build();
+
+        List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList());
+
+        assertThat(result).hasSize(1)
+            .extracting(JmapResponse::getResponse)
+            .hasOnlyElementsOfType(GetMessagesResponse.class)
+            .extracting(GetMessagesResponse.class::cast)
+            .flatExtracting(GetMessagesResponse::list)
+            .extracting(Message::getKeywords)
+            .containsOnly(
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true),
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true),
+                ImmutableMap.of(
+                    "$Answered", true,
+                    "$Draft", true));
+
+    }
+
+    @Test
+    public void processShouldReturnKeywordsWithoutForwardedWhenForwardedUserFlagsMessages() throws MailboxException {
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DELETED)
+            .add(FORWARDED)
+            .build();
+        MessageManager inbox = mailboxManager.getMailbox(inboxPath, session);
+        Date now = new Date();
+        ByteArrayInputStream message1Content = new ByteArrayInputStream("Subject: message 1 subject\r\n\r\nmy message".getBytes(Charsets.UTF_8));
+        ComposedMessageId message1 = inbox.appendMessage(message1Content, now, session, false, flags);
+
+        GetMessagesRequest request = GetMessagesRequest.builder()
+            .ids(ImmutableList.of(message1.getMessageId()))
+            .build();
+
+        List<JmapResponse> result = testee.process(request, clientId, session).collect(Collectors.toList());
+
+        assertThat(result).hasSize(1)
+            .extracting(JmapResponse::getResponse)
+            .hasOnlyElementsOfType(GetMessagesResponse.class)
+            .extracting(GetMessagesResponse.class::cast)
+            .flatExtracting(GetMessagesResponse::list)
+            .extracting(Message::getKeywords)
+            .containsOnly(
+                ImmutableMap.of(
+                    "$Answered", true,
+                    FORWARDED, true));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[06/13] james-project git commit: JAMES-2110 GetMessage should support keywords

Posted by bt...@apache.org.
JAMES-2110 GetMessage should support keywords


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

Branch: refs/heads/master
Commit: c1387efaea253d6f29577d31f8938f1ec6008841
Parents: 53fdcbd
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:40:11 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:27 2017 +0200

----------------------------------------------------------------------
 .../org/apache/james/jmap/model/Message.java    | 65 +++++---------
 .../apache/james/jmap/model/MessageFactory.java |  6 +-
 .../james/jmap/model/MessageProperties.java     |  1 +
 .../james/jmap/model/MessageFactoryTest.java    | 94 ++++++++++++++++++--
 .../james/jmap/model/MessagePropertiesTest.java | 10 ++-
 .../apache/james/jmap/model/MessageTest.java    | 65 +++++++-------
 6 files changed, 156 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Message.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Message.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Message.java
index 969525d..86762fe 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Message.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Message.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Predicate;
+import javax.mail.Flags;
 
 import org.apache.james.jmap.methods.GetMessagesMethod;
 import org.apache.james.jmap.methods.JmapResponseWriterImpl;
@@ -57,10 +58,6 @@ public class Message {
         private String threadId;
         private ImmutableList<MailboxId> mailboxIds;
         private String inReplyToMessageId;
-        private boolean isUnread;
-        private boolean isFlagged;
-        private boolean isAnswered;
-        private boolean isDraft;
         private ImmutableMap<String, String> headers;
         private Emailer from;
         private final ImmutableList.Builder<Emailer> to;
@@ -75,6 +72,7 @@ public class Message {
         private Optional<String> htmlBody = Optional.empty();
         private final ImmutableList.Builder<Attachment> attachments;
         private final ImmutableMap.Builder<BlobId, SubMessage> attachedMessages;
+        private Optional<Flags> flags = Optional.empty();
 
         private Builder() {
             to = ImmutableList.builder();
@@ -120,23 +118,8 @@ public class Message {
             return this;
         }
 
-        public Builder isUnread(boolean isUnread) {
-            this.isUnread = isUnread;
-            return this;
-        }
-
-        public Builder isFlagged(boolean isFlagged) {
-            this.isFlagged = isFlagged;
-            return this;
-        }
-
-        public Builder isAnswered(boolean isAnswered) {
-            this.isAnswered = isAnswered;
-            return this;
-        }
-
-        public Builder isDraft(boolean isDraft) {
-            this.isDraft = isDraft;
+        public Builder flags(Flags flags) {
+            this.flags = Optional.ofNullable(flags);
             return this;
         }
 
@@ -223,9 +206,15 @@ public class Message {
             ImmutableMap<BlobId, SubMessage> attachedMessages = this.attachedMessages.build();
             Preconditions.checkState(areAttachedMessagesKeysInAttachments(attachments, attachedMessages), "'attachedMessages' keys must be in 'attachements'");
             boolean hasAttachment = hasAttachment(attachments);
-
-            return new Message(id, blobId, threadId, mailboxIds, Optional.ofNullable(inReplyToMessageId), isUnread, isFlagged, isAnswered, isDraft, hasAttachment, headers, Optional.ofNullable(from),
-                    to.build(), cc.build(), bcc.build(), replyTo.build(), subject, date, size, preview, textBody, htmlBody, attachments, attachedMessages);
+            Keywords keywords = flags.map(flag -> Keywords.factory()
+                    .filterImapNonExposedKeywords()
+                    .fromFlags(flag))
+                .orElse(Keywords.DEFAULT_VALUE);
+
+            return new Message(id, blobId, threadId, mailboxIds, Optional.ofNullable(inReplyToMessageId),
+                hasAttachment, headers, Optional.ofNullable(from),
+                to.build(), cc.build(), bcc.build(), replyTo.build(), subject, date, size, preview, textBody, htmlBody, attachments, attachedMessages,
+                keywords);
         }
     }
 
@@ -250,10 +239,6 @@ public class Message {
     private final String threadId;
     private final ImmutableList<MailboxId> mailboxIds;
     private final Optional<String> inReplyToMessageId;
-    private final boolean isUnread;
-    private final boolean isFlagged;
-    private final boolean isAnswered;
-    private final boolean isDraft;
     private final boolean hasAttachment;
     @JsonFilter(GetMessagesMethod.HEADERS_FILTER)
     private final ImmutableMap<String, String> headers;
@@ -270,16 +255,13 @@ public class Message {
     private final Optional<String> htmlBody;
     private final ImmutableList<Attachment> attachments;
     private final ImmutableMap<BlobId, SubMessage> attachedMessages;
+    private final Keywords keywords;
 
     @VisibleForTesting Message(MessageId id,
                                BlobId blobId,
                                String threadId,
                                ImmutableList<MailboxId> mailboxIds,
                                Optional<String> inReplyToMessageId,
-                               boolean isUnread,
-                               boolean isFlagged,
-                               boolean isAnswered,
-                               boolean isDraft,
                                boolean hasAttachment,
                                ImmutableMap<String, String> headers,
                                Optional<Emailer> from,
@@ -294,16 +276,13 @@ public class Message {
                                Optional<String> textBody,
                                Optional<String> htmlBody,
                                ImmutableList<Attachment> attachments,
-                               ImmutableMap<BlobId, SubMessage> attachedMessages) {
+                               ImmutableMap<BlobId, SubMessage> attachedMessages,
+                               Keywords keywords) {
         this.id = id;
         this.blobId = blobId;
         this.threadId = threadId;
         this.mailboxIds = mailboxIds;
         this.inReplyToMessageId = inReplyToMessageId;
-        this.isUnread = isUnread;
-        this.isFlagged = isFlagged;
-        this.isAnswered = isAnswered;
-        this.isDraft = isDraft;
         this.hasAttachment = hasAttachment;
         this.headers = headers;
         this.from = from;
@@ -319,6 +298,7 @@ public class Message {
         this.htmlBody = htmlBody;
         this.attachments = attachments;
         this.attachedMessages = attachedMessages;
+        this.keywords = keywords;
     }
 
     public MessageId getId() {
@@ -342,19 +322,19 @@ public class Message {
     }
 
     public boolean isIsUnread() {
-        return isUnread;
+        return !keywords.contains(Keyword.SEEN);
     }
 
     public boolean isIsFlagged() {
-        return isFlagged;
+        return keywords.contains(Keyword.FLAGGED);
     }
 
     public boolean isIsAnswered() {
-        return isAnswered;
+        return keywords.contains(Keyword.ANSWERED);
     }
 
     public boolean isIsDraft() {
-        return isDraft;
+        return keywords.contains(Keyword.DRAFT);
     }
 
     public boolean isHasAttachment() {
@@ -417,4 +397,7 @@ public class Message {
         return attachedMessages;
     }
 
+    public ImmutableMap<String, Boolean> getKeywords() {
+        return keywords.asMap();
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java
index b5e5e1d..a84085b 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageFactory.java
@@ -30,7 +30,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-
 import javax.inject.Inject;
 import javax.mail.Flags;
 import javax.mail.internet.SharedInputStream;
@@ -94,10 +93,7 @@ public class MessageFactory {
                 .threadId(message.getMessageId().serialize())
                 .mailboxIds(message.getMailboxIds())
                 .inReplyToMessageId(getHeader(mimeMessage, "in-reply-to"))
-                .isUnread(! message.getFlags().contains(Flags.Flag.SEEN))
-                .isFlagged(message.getFlags().contains(Flags.Flag.FLAGGED))
-                .isAnswered(message.getFlags().contains(Flags.Flag.ANSWERED))
-                .isDraft(message.getFlags().contains(Flags.Flag.DRAFT))
+                .flags(message.getFlags())
                 .subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
                 .headers(toMap(mimeMessage.getHeader().getFields()))
                 .from(firstFromMailboxList(mimeMessage.getFrom()))

http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageProperties.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageProperties.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageProperties.java
index d8b23db..bf1bff5 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageProperties.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MessageProperties.java
@@ -143,6 +143,7 @@ public class MessageProperties {
         htmlBody("htmlBody"),
         attachments("attachments"),
         attachedMessages("attachedMessages"),
+        keywords("keywords"),
         body("body", PropertyType.INPUTONLY);
     
         private final String property;

http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java
index 4e0141d..76a5e14 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageFactoryTest.java
@@ -19,19 +19,16 @@
 package org.apache.james.jmap.model;
 
 import static org.assertj.core.api.Assertions.assertThat;
-
 import java.io.ByteArrayInputStream;
 import java.time.Instant;
 import java.util.Optional;
-
 import javax.mail.Flags;
 import javax.mail.Flags.Flag;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.james.jmap.model.MessageFactory.MetaDataWithContent;
 import org.apache.james.jmap.utils.HtmlTextExtractor;
 import org.apache.james.jmap.utils.JsoupHtmlTextExtractor;
+import org.apache.james.mailbox.FlagsBuilder;
 import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.model.AttachmentId;
@@ -39,14 +36,18 @@ import org.apache.james.mailbox.model.Cid;
 import org.apache.james.mailbox.model.MessageAttachment;
 import org.apache.james.mailbox.model.TestMessageId;
 import org.apache.james.util.mime.MessageContentExtractor;
-import org.junit.Before;
-import org.junit.Test;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
+import org.junit.Before;
+import org.junit.Test;
+
 public class MessageFactoryTest {
+    private static final String FORWARDED = "forwarded";
     private static final InMemoryId MAILBOX_ID = InMemoryId.of(18L);
     private static final Instant INTERNAL_DATE = Instant.parse("2012-02-03T14:30:42Z");
 
@@ -107,6 +108,7 @@ public class MessageFactoryTest {
 
     @Test
     public void headersShouldBeSetIntoMessage() throws Exception {
+        Flags flags = new Flags(Flag.SEEN);
         String headers = "From: user <us...@domain>\n"
                 + "Subject: test subject\n"
                 + "To: user1 <us...@domain>, user2 <us...@domain>\n"
@@ -118,7 +120,7 @@ public class MessageFactoryTest {
                 + "Other-header: other header value";
         MetaDataWithContent testMail = MetaDataWithContent.builder()
                 .uid(MessageUid.of(2))
-                .flags(new Flags(Flag.SEEN))
+                .flags(flags)
                 .size(headers.length())
                 .internalDate(INTERNAL_DATE)
                 .content(new ByteArrayInputStream(headers.getBytes(Charsets.UTF_8)))
@@ -145,6 +147,7 @@ public class MessageFactoryTest {
                 .put("Date", "Tue, 14 Jul 2015 12:30:42 +0000")
                 .put("MIME-Version", "1.0")
                 .build();
+
         Message testee = messageFactory.fromMetaDataWithContent(testMail);
         Message expected = Message.builder()
                 .id(TestMessageId.of(2))
@@ -164,12 +167,14 @@ public class MessageFactoryTest {
                 .preview("(Empty)")
                 .textBody(Optional.of(""))
                 .htmlBody(Optional.empty())
+                .flags(flags)
                 .build();
         assertThat(testee).isEqualToComparingFieldByField(expected);
     }
 
     @Test
     public void headersShouldBeUnfoldedAndDecoded() throws Exception {
+        Flags flags = new Flags(Flag.SEEN);
         String headers = "From: user <us...@domain>\n"
             + "Subject: test subject\n"
             + "To: user1 <us...@domain>,\r\n"
@@ -177,7 +182,7 @@ public class MessageFactoryTest {
             + "Cc: =?UTF-8?Q?Beno=c3=aet_TELLIER?= <te...@linagora.com>";
         MetaDataWithContent testMail = MetaDataWithContent.builder()
             .uid(MessageUid.of(2))
-            .flags(new Flags(Flag.SEEN))
+            .flags(flags)
             .size(headers.length())
             .internalDate(INTERNAL_DATE)
             .content(new ByteArrayInputStream(headers.getBytes(Charsets.UTF_8)))
@@ -213,7 +218,9 @@ public class MessageFactoryTest {
             .preview("(Empty)")
             .textBody(Optional.of(""))
             .htmlBody(Optional.empty())
+            .flags(flags)
             .build();
+
         assertThat(testee).isEqualToComparingFieldByField(expected);
     }
 
@@ -674,4 +681,75 @@ public class MessageFactoryTest {
             .extracting(Message::getPreview, Message::getHtmlBody, Message::getTextBody)
             .containsExactly("My plain message", Optional.of("<html></html>"), Optional.of("My plain message"));
     }
+
+    @Test
+    public void keywordShouldBeSetIntoMessage() throws Exception {
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DRAFT)
+            .build();
+        ImmutableMap<String, Boolean> keywords = Keywords.factory()
+            .fromFlags(flags)
+            .asMap();
+
+        MetaDataWithContent testMail = MetaDataWithContent.builder()
+            .uid(MessageUid.of(2))
+            .flags(flags)
+            .size(0)
+            .internalDate(INTERNAL_DATE)
+            .content(new ByteArrayInputStream("".getBytes(Charsets.UTF_8)))
+            .attachments(ImmutableList.of())
+            .mailboxId(MAILBOX_ID)
+            .messageId(TestMessageId.of(2))
+            .build();
+        Message testee = messageFactory.fromMetaDataWithContent(testMail);
+        assertThat(testee.getKeywords()).containsAllEntriesOf(keywords);
+    }
+
+    @Test
+    public void keywordWithUserFlagShouldBeSetIntoMessage() throws Exception {
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED)
+            .add(FORWARDED)
+            .build();
+        ImmutableMap<String, Boolean> keywords = Keywords.factory()
+            .fromFlags(flags)
+            .asMap();
+        MetaDataWithContent testMail = MetaDataWithContent.builder()
+            .uid(MessageUid.of(2))
+            .flags(flags)
+            .size(0)
+            .internalDate(INTERNAL_DATE)
+            .content(new ByteArrayInputStream("".getBytes(Charsets.UTF_8)))
+            .attachments(ImmutableList.of())
+            .mailboxId(MAILBOX_ID)
+            .messageId(TestMessageId.of(2))
+            .build();
+        Message testee = messageFactory.fromMetaDataWithContent(testMail);
+        assertThat(testee.getKeywords()).containsAllEntriesOf(keywords);
+    }
+
+    @Test
+    public void fromMetaDataWithContentShouldRemoveUnsupportedKeyword() throws Exception {
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DELETED, Flag.RECENT)
+            .add(FORWARDED)
+            .build();
+        ImmutableMap<String, Boolean> keywords = Keywords.factory()
+            .filterImapNonExposedKeywords()
+            .fromFlags(flags)
+            .asMap();
+
+        MetaDataWithContent testMail = MetaDataWithContent.builder()
+            .uid(MessageUid.of(2))
+            .flags(flags)
+            .size(0)
+            .internalDate(INTERNAL_DATE)
+            .content(new ByteArrayInputStream("".getBytes(Charsets.UTF_8)))
+            .attachments(ImmutableList.of())
+            .mailboxId(MAILBOX_ID)
+            .messageId(TestMessageId.of(2))
+            .build();
+        Message testee = messageFactory.fromMetaDataWithContent(testMail);
+        assertThat(testee.getKeywords()).containsAllEntriesOf(keywords);
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessagePropertiesTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessagePropertiesTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessagePropertiesTest.java
index 89c5e41..3e38e60 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessagePropertiesTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessagePropertiesTest.java
@@ -50,7 +50,15 @@ public class MessagePropertiesTest {
             .hasValueSatisfying(value -> 
                 assertThat(value).contains(MessageProperty.textBody).doesNotContain(MessageProperty.body));
     }
-    
+
+    @Test
+    public void toOutputPropertiesShouldReturnIsUnread() {
+        MessageProperties actual = new MessageProperties(Optional.of(ImmutableSet.of("isUnread"))).toOutputProperties();
+        assertThat(actual.getOptionalMessageProperties())
+                .hasValueSatisfying(value ->
+                        assertThat(value).contains(MessageProperty.isUnread));
+    }
+
     @Test
     public void toOutputPropertiesShouldReturnMandatoryPropertiesWhenEmptyRequest() {
         MessageProperties actual = new MessageProperties(Optional.of(ImmutableSet.of())).toOutputProperties();

http://git-wip-us.apache.org/repos/asf/james-project/blob/c1387efa/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageTest.java
index 494ae77..d1c0914 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MessageTest.java
@@ -23,6 +23,9 @@ import static org.assertj.core.api.Assertions.assertThat;
 import java.time.Instant;
 import java.util.Optional;
 
+import javax.mail.Flags;
+
+import org.apache.james.mailbox.FlagsBuilder;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.model.TestMessageId;
 import org.junit.Test;
@@ -101,8 +104,9 @@ public class MessageTest {
     @Test
     public void buildShouldWorkWhenMandatoryFieldsArePresent() {
         Instant currentDate = Instant.now();
-        Message expected = new Message(TestMessageId.of(1), BlobId.of("blobId"), "threadId", ImmutableList.of(InMemoryId.of(456)), Optional.empty(), false, false, false, false, false, ImmutableMap.of("key", "value"), Optional.empty(),
-                ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), "subject", currentDate, 123, "preview", Optional.empty(), Optional.empty(), ImmutableList.of(), ImmutableMap.of());
+        Message expected = new Message(TestMessageId.of(1), BlobId.of("blobId"), "threadId", ImmutableList.of(InMemoryId.of(456)), Optional.empty(), false, ImmutableMap.of("key", "value"), Optional.empty(),
+                ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), "subject", currentDate, 123, "preview", Optional.empty(), Optional.empty(),
+                ImmutableList.of(), ImmutableMap.of(), Keywords.DEFAULT_VALUE);
         Message tested = Message.builder()
                 .id(TestMessageId.of(1))
                 .blobId(BlobId.of("blobId"))
@@ -158,41 +162,42 @@ public class MessageTest {
                 .date(currentDate)
                 .build();
         ImmutableMap<BlobId, SubMessage> attachedMessages = ImmutableMap.of(BlobId.of("blobId"), simpleMessage);
+        Flags flags = FlagsBuilder.builder()
+            .add(Flags.Flag.DRAFT, Flags.Flag.ANSWERED, Flags.Flag.FLAGGED)
+            .build();
+
+        Keywords keywords = Keywords.factory()
+            .fromFlags(flags);
+
         Message expected = new Message(
-                TestMessageId.of(1),
-                BlobId.of("blobId"),
-                "threadId",
-                ImmutableList.of(InMemoryId.of(456)),
-                Optional.of("inReplyToMessageId"), 
-                true,
-                true,
-                true,
-                true,
-                true,
-                ImmutableMap.of("key", "value"),
-                Optional.of(from),
-                to,
-                cc,
-                bcc,
-                replyTo,
-                "subject",
-                currentDate,
-                123,
-                "preview",
-                Optional.of("textBody"), 
-                Optional.of("htmlBody"),
-                attachments,
-                attachedMessages);
+            TestMessageId.of(1),
+            BlobId.of("blobId"),
+            "threadId",
+            ImmutableList.of(InMemoryId.of(456)),
+            Optional.of("inReplyToMessageId"),
+            true,
+            ImmutableMap.of("key", "value"),
+            Optional.of(from),
+            to,
+            cc,
+            bcc,
+            replyTo,
+            "subject",
+            currentDate,
+            123,
+            "preview",
+            Optional.of("textBody"),
+            Optional.of("htmlBody"),
+            attachments,
+            attachedMessages,
+            keywords);
         Message tested = Message.builder()
             .id(TestMessageId.of(1))
             .blobId(BlobId.of("blobId"))
             .threadId("threadId")
             .mailboxId(InMemoryId.of(456))
             .inReplyToMessageId("inReplyToMessageId")
-            .isUnread(true)
-            .isFlagged(true)
-            .isAnswered(true)
-            .isDraft(true)
+            .flags(flags)
             .headers(ImmutableMap.of("key", "value"))
             .from(from)
             .to(to)


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[12/13] james-project git commit: JAMES-2046 Add a test for SentDent ordering for MessageSearchIndexes

Posted by bt...@apache.org.
JAMES-2046 Add a test for SentDent ordering for MessageSearchIndexes


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

Branch: refs/heads/master
Commit: 5af0d27aad7f210a8b6245e84915f683937b51b8
Parents: df4d1d8
Author: benwa <bt...@linagora.com>
Authored: Thu Aug 24 14:07:03 2017 +0700
Committer: benwa <bt...@linagora.com>
Committed: Fri Aug 25 15:31:47 2017 +0700

----------------------------------------------------------------------
 .../search/AbstractMessageSearchIndexTest.java  | 55 ++++++++++++++++++++
 1 file changed, 55 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/5af0d27a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
index 5dbfe9a..fd9d173 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
@@ -21,6 +21,8 @@ package org.apache.james.mailbox.store.search;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.ByteArrayInputStream;
+import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 
@@ -29,6 +31,7 @@ import javax.mail.Flags;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.ComposedMessageId;
 import org.apache.james.mailbox.model.MailboxConstants;
@@ -1143,4 +1146,56 @@ public abstract class AbstractMessageSearchIndexTest {
 
         assertThat(messageSearchIndex.search(otherSession, mailbox, searchQuery)).isEmpty();
     }
+
+    @Test
+    public void searchShouldOrderByInternalDateWhenSortOnSentDateAndNoCorrespondingHeader() throws Exception {
+        MailboxPath mailboxPath = new MailboxPath(MailboxConstants.USER_NAMESPACE, USERNAME, "sentDate");
+        storeMailboxManager.createMailbox(mailboxPath, session);
+
+        MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+        boolean isRecent = false;
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+        Date date1 = simpleDateFormat.parse("2017-08-24");
+        Date date2 = simpleDateFormat.parse("2017-08-23");
+        Date date3 = simpleDateFormat.parse("2017-08-25");
+        ComposedMessageId message1 = messageManager.appendMessage(new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), date1, session, isRecent, new Flags());
+        ComposedMessageId message2 = messageManager.appendMessage(new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), date2, session, isRecent, new Flags());
+        ComposedMessageId message3 = messageManager.appendMessage(new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), date3, session, isRecent, new Flags());
+
+        await();
+
+        SearchQuery searchQuery = new SearchQuery();
+        searchQuery.setSorts(ImmutableList.of(new Sort(SortClause.SentDate)));
+
+        assertThat(messageManager.search(searchQuery, session))
+            .containsExactly(message2.getUid(),
+                message1.getUid(),
+                message3.getUid());
+    }
+
+    @Test
+    public void searchShouldOrderBySentDateThenInternalDateWhenSortOnSentDateAndNonHomogeneousCorrespondingHeader() throws Exception {
+        MailboxPath mailboxPath = new MailboxPath(MailboxConstants.USER_NAMESPACE, USERNAME, "sentDate");
+        storeMailboxManager.createMailbox(mailboxPath, session);
+
+        MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+        boolean isRecent = false;
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+        Date date1 = simpleDateFormat.parse("2017-08-24");
+        Date date2 = simpleDateFormat.parse("2017-08-26");
+        Date date3 = simpleDateFormat.parse("2017-08-25");
+        ComposedMessageId message1 = messageManager.appendMessage(new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), date1, session, isRecent, new Flags());
+        ComposedMessageId message2 = messageManager.appendMessage(new ByteArrayInputStream("Date: Wed, 23 Aug 2017 00:00:00 +0200\r\nSubject: test\r\n\r\ntestmail".getBytes()), date2, session, isRecent, new Flags());
+        ComposedMessageId message3 = messageManager.appendMessage(new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), date3, session, isRecent, new Flags());
+
+        await();
+
+        SearchQuery searchQuery = new SearchQuery();
+        searchQuery.setSorts(ImmutableList.of(new Sort(SortClause.SentDate)));
+
+        assertThat(messageManager.search(searchQuery, session))
+            .containsExactly(message2.getUid(),
+                message1.getUid(),
+                message3.getUid());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[04/13] james-project git commit: JAMES-2110 SetMessage with update should support keywords for flags

Posted by bt...@apache.org.
JAMES-2110 SetMessage with update should support keywords for flags


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

Branch: refs/heads/master
Commit: 92365a2ee4813062a37f113a308ea172bc84025e
Parents: 78a4a57
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:43:27 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:27 2017 +0200

----------------------------------------------------------------------
 .../methods/SetMessagesUpdateProcessor.java     |   7 +
 .../james/jmap/model/UpdateMessagePatch.java    |  72 ++++----
 .../jmap/model/UpdateMessagePatchTest.java      | 184 +++++++++++++++++--
 3 files changed, 206 insertions(+), 57 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/92365a2e/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
index c303359..da7dceb 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
@@ -111,6 +111,13 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
             }
         } catch (MailboxException e) {
             handleMessageUpdateException(messageId, builder, e);
+        } catch (IllegalArgumentException e) {
+            ValidationResult invalidPropertyKeywords = ValidationResult.builder()
+                    .property(MessageProperties.MessageProperty.keywords.asFieldName())
+                    .message(e.getMessage())
+                    .build();
+
+            handleInvalidRequest(builder, messageId, ImmutableList.of(invalidPropertyKeywords));
         }
 
     }

http://git-wip-us.apache.org/repos/asf/james-project/blob/92365a2e/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java
index 32e3fe2..6a46bf8 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java
@@ -20,9 +20,9 @@
 package org.apache.james.jmap.model;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
 import javax.mail.Flags;
 
 import org.apache.james.jmap.methods.ValidationResult;
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
@@ -47,6 +48,7 @@ public class UpdateMessagePatch {
         private Optional<Boolean> isFlagged = Optional.empty();
         private Optional<Boolean> isUnread = Optional.empty();
         private Optional<Boolean> isAnswered = Optional.empty();
+        private Optional<Map<String, Boolean>> keywords = Optional.empty();
         private Set<ValidationResult> validationResult = Sets.newHashSet();
 
         public Builder mailboxIds(List<String> mailboxIds) {
@@ -54,6 +56,11 @@ public class UpdateMessagePatch {
             return this;
         }
 
+        public Builder keywords(Map<String, Boolean> keywords) {
+            this.keywords = Optional.of(ImmutableMap.copyOf(keywords));
+            return this;
+        }
+
         public Builder isFlagged(Boolean isFlagged) {
             this.isFlagged = Optional.of(isFlagged);
             return this;
@@ -81,28 +88,36 @@ public class UpdateMessagePatch {
                     .message("mailboxIds property is not supposed to be empty")
                     .build()));
             }
-            return new UpdateMessagePatch(mailboxIds, isUnread, isFlagged, isAnswered, ImmutableList.copyOf(validationResult));
+
+            Optional<Keywords> updateKeywords = Keywords.factory()
+                .throwOnImapNonExposedKeywords()
+                .fromMapOrOldKeyword(keywords, getOldKeywords());
+
+            return new UpdateMessagePatch(mailboxIds, updateKeywords, ImmutableList.copyOf(validationResult));
+        }
+
+        private Optional<OldKeyword> getOldKeywords() {
+            if (isAnswered.isPresent() || isFlagged.isPresent() || isUnread.isPresent()) {
+                Optional<Boolean> isDraft = Optional.empty();
+                return Optional.of(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+            }
+            return Optional.empty();
         }
+
     }
 
     private final Optional<List<String>> mailboxIds;
-    private final Optional<Boolean> isUnread;
-    private final Optional<Boolean> isFlagged;
-    private final Optional<Boolean> isAnswered;
+    private final Optional<Keywords> keywords;
 
     private final ImmutableList<ValidationResult> validationErrors;
 
     @VisibleForTesting
     UpdateMessagePatch(Optional<List<String>> mailboxIds,
-                       Optional<Boolean> isUnread,
-                       Optional<Boolean> isFlagged,
-                       Optional<Boolean> isAnswered,
+                       Optional<Keywords> keywords,
                        ImmutableList<ValidationResult> validationResults) {
 
         this.mailboxIds = mailboxIds;
-        this.isUnread = isUnread;
-        this.isFlagged = isFlagged;
-        this.isAnswered = isAnswered;
+        this.keywords = keywords;
         this.validationErrors = validationResults;
     }
 
@@ -110,22 +125,12 @@ public class UpdateMessagePatch {
         return mailboxIds;
     }
 
-    public Optional<Boolean> isUnread() {
-        return isUnread;
-    }
-
-    public Optional<Boolean> isFlagged() {
-        return isFlagged;
-    }
-
-    public Optional<Boolean> isAnswered() {
-        return isAnswered;
+    public Optional<Keywords> getKeywords() {
+        return keywords;
     }
 
     public boolean isFlagsIdentity() {
-        return !isAnswered.isPresent()
-            && !isFlagged.isPresent()
-            && !isUnread.isPresent();
+        return !keywords.isPresent();
     }
 
     public ImmutableList<ValidationResult> getValidationErrors() {
@@ -137,19 +142,12 @@ public class UpdateMessagePatch {
     }
 
     public Flags applyToState(Flags currentFlags) {
-        Flags newStateFlags = new Flags();
-
-        if (isFlagged().orElse(currentFlags.contains(Flags.Flag.FLAGGED))) {
-            newStateFlags.add(Flags.Flag.FLAGGED);
-        }
-        if (isAnswered().orElse(currentFlags.contains(Flags.Flag.ANSWERED))) {
-            newStateFlags.add(Flags.Flag.ANSWERED);
-        }
-        boolean shouldMessageBeMarkSeen = isUnread().map(b -> !b).orElse(currentFlags.contains(Flags.Flag.SEEN));
-        if (shouldMessageBeMarkSeen) {
-            newStateFlags.add(Flags.Flag.SEEN);
-        }
-        return newStateFlags;
+        return keywords.map(keyword -> {
+            if (currentFlags.contains(Flags.Flag.DRAFT) != keyword.getKeywords().contains(Keyword.DRAFT)) {
+                throw new IllegalArgumentException("Cannot add or remove draft flag");
+            }
+            return keyword.asFlagsWithRecentAndDeletedFrom(currentFlags);
+        }).orElse(new Flags());
     }
 
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/92365a2e/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/UpdateMessagePatchTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/UpdateMessagePatchTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/UpdateMessagePatchTest.java
index e97c49c..e227746 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/UpdateMessagePatchTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/UpdateMessagePatchTest.java
@@ -20,42 +20,52 @@
 package org.apache.james.jmap.model;
 
 import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.Arrays;
 import java.util.List;
-
 import javax.mail.Flags;
 
+import org.apache.james.mailbox.FlagsBuilder;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 public class UpdateMessagePatchTest {
+    private final static String FORWARDED = "forwarded";
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
 
     @Test
     public void UnsetUpdatePatchShouldBeValid() {
         UpdateMessagePatch emptyPatch = UpdateMessagePatch.builder().build();
         assertThat(emptyPatch.isValid()).isTrue();
-        assertThat(emptyPatch.isUnread()).isEmpty();
-        assertThat(emptyPatch.isFlagged()).isEmpty();
-        assertThat(emptyPatch.isAnswered()).isEmpty();
     }
 
     @Test
     public void builderShouldSetUnreadFalseWhenBuiltWithIsUnreadFalse() {
         UpdateMessagePatch testee = UpdateMessagePatch.builder().isUnread(false).build();
-        assertThat(testee.isUnread()).isPresent();
-        assertThat(testee.isUnread().get()).isFalse();
     }
 
     @Test
-    public void applyStateShouldSetFlaggedOnlyWhenUnsetPatchAppliedToFlaggedState() {
-        UpdateMessagePatch testee = UpdateMessagePatch.builder().build();
-        Flags isFlaggedSet = new Flags(Flags.Flag.FLAGGED);
-        List<Flags.Flag> updatedFlags = Arrays.asList(testee.applyToState(isFlaggedSet).getSystemFlags());
+    public void applyStateShouldSetFlaggedOnlyWhenIsFlagged() {
+        UpdateMessagePatch testee = UpdateMessagePatch.builder().isFlagged(true).build();
+        List<Flags.Flag> updatedFlags = Arrays.asList(testee.applyToState(new Flags()).getSystemFlags());
         assertThat(updatedFlags).containsExactly(Flags.Flag.FLAGGED);
     }
 
 
     @Test
+    public void applyStateShouldRemoveFlaggedWhenEmptyIsFlaggedOnFlaggedMessage() {
+        UpdateMessagePatch testee = UpdateMessagePatch.builder().isAnswered(true).build();
+        Flags isFlagged = new Flags(Flags.Flag.FLAGGED);
+        List<Flags.Flag> updatedFlags = Arrays.asList(testee.applyToState(isFlagged).getSystemFlags());
+        assertThat(updatedFlags).doesNotContain(Flags.Flag.FLAGGED);
+    }
+
+    @Test
     public void applyStateShouldReturnUnreadFlagWhenUnreadSetOnSeenMessage() {
         UpdateMessagePatch testee = UpdateMessagePatch.builder().isUnread(true).build();
         Flags isSeen = new Flags(Flags.Flag.SEEN);
@@ -64,14 +74,6 @@ public class UpdateMessagePatchTest {
     }
 
     @Test
-    public void applyStateShouldReturnFlaggedWhenEmptyPatchOnFlaggedMessage() {
-        UpdateMessagePatch testee = UpdateMessagePatch.builder().build();
-        Flags isFlagged = new Flags(Flags.Flag.FLAGGED);
-        List<Flags.Flag> updatedFlags = Arrays.asList(testee.applyToState(isFlagged).getSystemFlags());
-        assertThat(updatedFlags).containsExactly(Flags.Flag.FLAGGED);
-    }
-
-    @Test
     public void applyStateShouldReturnSeenWhenPatchSetsSeenOnSeenMessage() {
         UpdateMessagePatch testee = UpdateMessagePatch.builder().isUnread(false).build();
         Flags isSeen = new Flags(Flags.Flag.SEEN);
@@ -80,7 +82,116 @@ public class UpdateMessagePatchTest {
     }
 
     @Test
-    public void isIdentityShouldReturnTrueWhenNoFlags() {
+    public void applyStateShouldReturnNewFlagsWhenKeywords() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Answered", true,
+                "$Flagged", true);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+        Flags isSeen = new Flags(Flags.Flag.SEEN);
+        assertThat(testee.applyToState(isSeen).getSystemFlags())
+            .containsExactly(Flags.Flag.ANSWERED, Flags.Flag.FLAGGED);
+    }
+
+    @Test
+    public void applyStateShouldReturnRemoveFlagsWhenKeywords() {
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(ImmutableMap.of())
+            .build();
+        Flags isSeen = new Flags(Flags.Flag.SEEN);
+        assertThat(testee.applyToState(isSeen).getSystemFlags()).isEmpty();
+    }
+
+    @Test
+    public void applyStateShouldThrowWhenKeywordsContainDeletedFlag() {
+        expectedException.expect(IllegalArgumentException.class);
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Deleted", true);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+
+        Flags currentFlags = new Flags(Flags.Flag.SEEN);
+
+        testee.applyToState(currentFlags);
+    }
+
+    @Test
+    public void applyStateShouldThrowWhenKeywordsContainRecentFlag() {
+        expectedException.expect(IllegalArgumentException.class);
+
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Recent", true);
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+
+        Flags currentFlags = new Flags(Flags.Flag.SEEN);
+
+        testee.applyToState(currentFlags);
+    }
+
+    @Test
+    public void applyStateShouldReturnFlagsWithoutUserFlagWhenKeywordsContainForwarded() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Answered", Keyword.FLAG_VALUE,
+                FORWARDED, Keyword.FLAG_VALUE);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+        Flags isSeen = new Flags(Flags.Flag.SEEN);
+        assertThat(testee.applyToState(isSeen).getSystemFlags())
+            .doesNotContain(Flags.Flag.USER);
+    }
+
+    @Test
+    public void applyStateShouldReturnFlagsWithUserFlagStringWhenKeywordsContainForwarded() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Answered", Keyword.FLAG_VALUE,
+                FORWARDED, Keyword.FLAG_VALUE);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+        Flags isSeen = new Flags(Flags.Flag.SEEN);
+        assertThat(testee.applyToState(isSeen).getUserFlags())
+            .containsExactly(FORWARDED);
+    }
+
+    @Test
+    public void applyStateShouldReturnFlagsWithDeletedFlagWhenKeywordsDoNotContainDeletedButOriginFlagsHaveDeleted() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Answered", Keyword.FLAG_VALUE);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+        Flags isSeen = new Flags(Flags.Flag.DELETED);
+        assertThat(testee.applyToState(isSeen).getSystemFlags())
+            .containsOnly(Flags.Flag.DELETED, Flags.Flag.ANSWERED);
+    }
+
+    @Test
+    public void applyStateShouldReturnFlagsWithRecentFlagWhenKeywordsDoNotContainRecentButOriginFlagsHaveRecent() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+                "$Answered", Keyword.FLAG_VALUE);
+
+        UpdateMessagePatch testee = UpdateMessagePatch.builder()
+            .keywords(keywords)
+            .build();
+        Flags flags = FlagsBuilder.builder()
+            .add(Flags.Flag.DELETED, Flags.Flag.RECENT)
+            .build();
+        assertThat(testee.applyToState(flags).getSystemFlags())
+            .containsOnly(Flags.Flag.DELETED, Flags.Flag.RECENT, Flags.Flag.ANSWERED);
+    }
+
+    @Test
+    public void isIdentityShouldReturnTrueWhenNoFlagsAndEmptyKeywords() {
         UpdateMessagePatch messagePatch = UpdateMessagePatch.builder().build();
 
         assertThat(messagePatch.isFlagsIdentity()).isTrue();
@@ -113,4 +224,37 @@ public class UpdateMessagePatchTest {
         assertThat(messagePatch.isFlagsIdentity()).isFalse();
     }
 
+    @Test
+    public void isIdentityShouldReturnFalseWhenKeywords() {
+        ImmutableMap<String, Boolean> keywords = ImmutableMap.of(
+            "$Answered", Keyword.FLAG_VALUE,
+            "$Flagged", Keyword.FLAG_VALUE);
+
+        UpdateMessagePatch messagePatch = UpdateMessagePatch.builder()
+                .keywords(keywords)
+                .build();
+
+        assertThat(messagePatch.isFlagsIdentity()).isFalse();
+    }
+
+    @Test
+    public void isIdentityShouldReturnFalseWhenEmptyKeywords() {
+        UpdateMessagePatch messagePatch = UpdateMessagePatch.builder()
+                .keywords(ImmutableMap.of())
+                .build();
+
+        assertThat(messagePatch.isFlagsIdentity()).isFalse();
+    }
+
+    @Test
+    public void isIdentityShouldThrowWhenBothIsFlagAndKeywords() {
+        expectedException.expect(IllegalArgumentException.class);
+
+        UpdateMessagePatch messagePatch = UpdateMessagePatch.builder()
+                .keywords(ImmutableMap.of())
+                .isAnswered(false)
+                .build();
+
+        messagePatch.isFlagsIdentity();
+    }
 }
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[07/13] james-project git commit: JAMES-2110 IT for setMessage with keywords

Posted by bt...@apache.org.
JAMES-2110 IT for setMessage with keywords


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

Branch: refs/heads/master
Commit: 37cf8ffc389219c55793dffc5692f898dad331b1
Parents: a6f1c10
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:46:53 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:28 2017 +0200

----------------------------------------------------------------------
 .../james/mailbox/store/probe/MailboxProbe.java |   2 +-
 .../james/cli/probe/impl/JmxMailboxProbe.java   |   9 +-
 .../apache/james/modules/MailboxProbeImpl.java  |   6 +-
 .../modules/protocols/JMAPServerModule.java     |   2 +
 .../org/apache/james/utils/MessageIdProbe.java  |  50 ++
 .../integration/SetMessagesMethodTest.java      | 587 +++++++++++++++++--
 6 files changed, 593 insertions(+), 63 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/mailbox/store/src/main/java/org/apache/james/mailbox/store/probe/MailboxProbe.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/probe/MailboxProbe.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/probe/MailboxProbe.java
index 6878bb5..b96671e 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/probe/MailboxProbe.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/probe/MailboxProbe.java
@@ -41,7 +41,7 @@ public interface MailboxProbe {
     void deleteMailbox(String namespace, String user, String name);
 
     void importEmlFileToMailbox(String namespace, String user, String name, String emlpath) throws Exception;
-	
+
     ComposedMessageId appendMessage(String username, MailboxPath mailboxPath, InputStream message, Date internalDate,
             boolean isRecent, Flags flags) throws MailboxException;
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/server/container/cli/src/main/java/org/apache/james/cli/probe/impl/JmxMailboxProbe.java
----------------------------------------------------------------------
diff --git a/server/container/cli/src/main/java/org/apache/james/cli/probe/impl/JmxMailboxProbe.java b/server/container/cli/src/main/java/org/apache/james/cli/probe/impl/JmxMailboxProbe.java
index b3b5cc5..ab9005a 100644
--- a/server/container/cli/src/main/java/org/apache/james/cli/probe/impl/JmxMailboxProbe.java
+++ b/server/container/cli/src/main/java/org/apache/james/cli/probe/impl/JmxMailboxProbe.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.Date;
-
 import javax.mail.Flags;
 import javax.management.MalformedObjectNameException;
 
@@ -37,15 +36,15 @@ import org.apache.james.mailbox.store.mail.model.Mailbox;
 import org.apache.james.mailbox.store.probe.MailboxProbe;
 
 public class JmxMailboxProbe implements MailboxProbe, JmxProbe {
-    
+
     private final static String MAILBOXCOPIER_OBJECT_NAME = "org.apache.james:type=component,name=mailboxcopier";
     private final static String MAILBOXMANAGER_OBJECT_NAME = "org.apache.james:type=component,name=mailboxmanagerbean";
     private final static String REINDEXER_OBJECT_NAME = "org.apache.james:type=component,name=reindexerbean";
-    
+
     private MailboxCopierManagementMBean mailboxCopierManagement;
     private MailboxManagerManagementMBean mailboxManagerManagement;
     private ReIndexerManagementMBean reIndexerManagement;
-    
+
     public JmxMailboxProbe connect(JmxConnection jmxc) throws IOException {
         try {
             mailboxCopierManagement = jmxc.retrieveBean(MailboxCopierManagementMBean.class, MAILBOXCOPIER_OBJECT_NAME);
@@ -56,7 +55,7 @@ public class JmxMailboxProbe implements MailboxProbe, JmxProbe {
         }
         return this;
     }
-    
+
 
     @Override
     public void copyMailbox(String srcBean, String dstBean) throws Exception {

http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
----------------------------------------------------------------------
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
index 73982a7..6dbfb05 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
@@ -155,11 +155,11 @@ public class MailboxProbeImpl implements GuiceProbe, MailboxProbe {
         mailboxManager.endProcessingRequest(mailboxSession);
         mailboxSession.close();
     }
-    
+
     @Override
-    public ComposedMessageId appendMessage(String username, MailboxPath mailboxPath, InputStream message, Date internalDate, boolean isRecent, Flags flags) 
+    public ComposedMessageId appendMessage(String username, MailboxPath mailboxPath, InputStream message, Date internalDate, boolean isRecent, Flags flags)
             throws MailboxException {
-        
+
         MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
         MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, mailboxSession);
         return messageManager.appendMessage(message, internalDate, mailboxSession, isRecent, flags);

http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JMAPServerModule.java
----------------------------------------------------------------------
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JMAPServerModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JMAPServerModule.java
index aa1cc00..c601c44 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JMAPServerModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JMAPServerModule.java
@@ -26,6 +26,7 @@ import org.apache.commons.configuration.HierarchicalConfiguration;
 import org.apache.james.jmap.JMAPConfiguration;
 import org.apache.james.jmap.JMAPModule;
 import org.apache.james.jmap.JMAPServer;
+import org.apache.james.utils.MessageIdProbe;
 import org.apache.james.jmap.crypto.JamesSignatureHandler;
 import org.apache.james.lifecycle.api.Configurable;
 import org.apache.james.utils.ConfigurationPerformer;
@@ -49,6 +50,7 @@ public class JMAPServerModule extends AbstractModule {
         install(new JMAPModule());
         Multibinder.newSetBinder(binder(), ConfigurationPerformer.class).addBinding().to(JMAPModuleConfigurationPerformer.class);
         Multibinder.newSetBinder(binder(), GuiceProbe.class).addBinding().to(JmapGuiceProbe.class);
+        Multibinder.newSetBinder(binder(), GuiceProbe.class).addBinding().to(MessageIdProbe.class);
     }
 
     @Singleton

http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/server/container/guice/protocols/jmap/src/main/java/org/apache/james/utils/MessageIdProbe.java
----------------------------------------------------------------------
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/utils/MessageIdProbe.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/utils/MessageIdProbe.java
new file mode 100644
index 0000000..f6b97a2
--- /dev/null
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/utils/MessageIdProbe.java
@@ -0,0 +1,50 @@
+/****************************************************************
+ * 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.utils;
+
+import java.util.List;
+import javax.inject.Inject;
+
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.FetchGroupImpl;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageResult;
+
+import com.google.common.collect.ImmutableList;
+
+public class MessageIdProbe implements GuiceProbe {
+    private final MailboxManager mailboxManager;
+    private final MessageIdManager messageIdManager;
+
+    @Inject
+    public MessageIdProbe(MailboxManager mailboxManager, MessageIdManager messageIdManager) {
+        this.mailboxManager = mailboxManager;
+        this.messageIdManager = messageIdManager;
+    }
+
+    public List<MessageResult> getMessages(MessageId messageId, String user) throws MailboxException {
+        MailboxSession mailboxSession = mailboxManager.createSystemSession(user);
+
+        return messageIdManager.getMessages(ImmutableList.of(messageId), FetchGroupImpl.FULL_CONTENT, mailboxSession);
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/37cf8ffc/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
index f372583..3fe34bb 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
@@ -26,6 +26,7 @@ import static com.jayway.restassured.config.RestAssuredConfig.newConfig;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.endsWith;
 import static org.hamcrest.Matchers.equalTo;
@@ -38,7 +39,6 @@ import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.collection.IsMapWithSize.aMapWithSize;
 import static org.hamcrest.collection.IsMapWithSize.anEmptyMap;
-
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.time.ZonedDateTime;
@@ -47,16 +47,15 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
-
 import javax.mail.Flags;
+import javax.mail.Flags.Flag;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.http.client.utils.URIBuilder;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.jmap.DefaultMailboxes;
 import org.apache.james.jmap.HttpJmapAuthentication;
 import org.apache.james.jmap.api.access.AccessToken;
 import org.apache.james.jmap.model.mailbox.Role;
+import org.apache.james.mailbox.FlagsBuilder;
 import org.apache.james.mailbox.MailboxListener;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.Attachment;
@@ -64,20 +63,18 @@ import org.apache.james.mailbox.model.ComposedMessageId;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageResult;
 import org.apache.james.mailbox.store.event.EventFactory;
 import org.apache.james.mailbox.store.mail.model.Mailbox;
 import org.apache.james.mailbox.store.probe.MailboxProbe;
 import org.apache.james.modules.MailboxProbeImpl;
+import org.apache.james.utils.MessageIdProbe;
 import org.apache.james.probe.DataProbe;
 import org.apache.james.util.ZeroedInputStream;
-import org.apache.james.utils.JmapGuiceProbe;
 import org.apache.james.utils.DataProbeImpl;
-import org.hamcrest.Matcher;
-import org.hamcrest.Matchers;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.apache.james.utils.JmapGuiceProbe;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.client.utils.URIBuilder;
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
@@ -91,9 +88,16 @@ import com.jayway.restassured.builder.RequestSpecBuilder;
 import com.jayway.restassured.builder.ResponseSpecBuilder;
 import com.jayway.restassured.http.ContentType;
 import com.jayway.restassured.specification.ResponseSpecification;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
 
 public abstract class SetMessagesMethodTest {
 
+    private final static String FORWARDED = "$Forwarded";
     private static final int _1MB = 1024*1024;
     private static final String NAME = "[0][0]";
     private static final String ARGUMENTS = "[0][1]";
@@ -103,27 +107,30 @@ public abstract class SetMessagesMethodTest {
     private static final String USERNAME = "username@" + USERS_DOMAIN;
     public static final MailboxPath USER_MAILBOX = new MailboxPath(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
     private static final String NOT_UPDATED = ARGUMENTS + ".notUpdated";
+    private static final String NOT_CREATED = ARGUMENTS + ".notCreated";
 
     private ConditionFactory calmlyAwait;
 
     protected abstract GuiceJamesServer createJmapServer();
 
     protected abstract MessageId randomMessageId();
-    
+
     protected abstract void await();
 
     private AccessToken accessToken;
     private GuiceJamesServer jmapServer;
     private MailboxProbe mailboxProbe;
     private DataProbe dataProbe;
-    
+    private MessageIdProbe messageProbe;
+
     @Before
     public void setup() throws Throwable {
         jmapServer = createJmapServer();
         jmapServer.start();
         mailboxProbe = jmapServer.getProbe(MailboxProbeImpl.class);
         dataProbe = jmapServer.getProbe(DataProbeImpl.class);
-        
+        messageProbe = jmapServer.getProbe(MessageIdProbe.class);
+
         RestAssured.requestSpecification = new RequestSpecBuilder()
                 .setContentType(ContentType.JSON)
                 .setAccept(ContentType.JSON)
@@ -161,7 +168,7 @@ public abstract class SetMessagesMethodTest {
     public void teardown() {
         jmapServer.stop();
     }
-    
+
     private String getOutboxId(AccessToken accessToken) {
         return getMailboxId(accessToken, Role.OUTBOX);
     }
@@ -172,7 +179,7 @@ public abstract class SetMessagesMethodTest {
             .map(x -> x.get("id"))
             .findFirst().get();
     }
-    
+
     private List<Map<String, String>> getAllMailboxesIds(AccessToken accessToken) {
         return with()
             .header("Authorization", accessToken.serialize())
@@ -185,7 +192,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessagesShouldReturnErrorNotSupportedWhenRequestContainsNonNullAccountId() throws Exception {
+    public void setMessagesShouldReturnAnErrorNotSupportedWhenRequestContainsNonNullAccountId() throws Exception {
         given()
             .header("Authorization", accessToken.serialize())
             .body("[[\"setMessages\", {\"accountId\": \"1\"}, \"#0\"]]")
@@ -199,7 +206,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessagesShouldReturnErrorNotSupportedWhenRequestContainsNonNullIfInState() throws Exception {
+    public void setMessagesShouldReturnAnErrorNotSupportedWhenRequestContainsNonNullIfInState() throws Exception {
         given()
             .header("Authorization", accessToken.serialize())
             .body("[[\"setMessages\", {\"ifInState\": \"1\"}, \"#0\"]]")
@@ -369,7 +376,7 @@ public abstract class SetMessagesMethodTest {
                 randomMessageId().serialize(),
                 message3.getMessageId().serialize()))
         .post("/jmap");
-        
+
         // Then
         given()
             .header("Authorization", accessToken.serialize())
@@ -396,7 +403,7 @@ public abstract class SetMessagesMethodTest {
         await();
 
         String serializedMessageId = message.getMessageId().serialize();
-        
+
         // When
         given()
             .header("Authorization", accessToken.serialize())
@@ -409,6 +416,322 @@ public abstract class SetMessagesMethodTest {
             .spec(getSetMessagesUpdateOKResponseAssertions(serializedMessageId));
     }
 
+    @Test
+    public void setMessagesWithUpdateShouldReturnAnErrorWhenBothIsFlagAndKeywordsArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : false, \"keywords\": {\"$Seen\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", containsString("Does not support keyword and is* at the same time"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldUpdateKeywordsWhenKeywordsArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
+        await();
+
+        String serializedMessageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Seen\": true, \"$Flagged\": true} } } }, \"#0\"]]", serializedMessageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(serializedMessageId));
+
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + serializedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].keywords.$Seen", equalTo(true))
+            .body(ARGUMENTS + ".list[0].keywords.$Flagged", equalTo(true));
+    }
+
+    @Test
+    public void setMessagesShouldAddForwardedFlagWhenKeywordsWithForwardedIsPassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
+        await();
+
+        String serializedMessageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Seen\": true, \"$Forwarded\": true} } } }, \"#0\"]]", serializedMessageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(serializedMessageId));
+
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + serializedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].keywords.$Seen", equalTo(true))
+            .body(ARGUMENTS + ".list[0].keywords.$Forwarded", equalTo(true));
+    }
+
+    @Test
+    public void setMessagesShouldRemoveForwardedFlagWhenKeywordsWithoutForwardedIsPassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        Flags flags = FlagsBuilder.builder()
+                .add(Flag.SEEN)
+                .add(FORWARDED)
+                .build();
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, flags);
+        await();
+
+        String serializedMessageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Seen\": true} } } }, \"#0\"]]", serializedMessageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(serializedMessageId));
+
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + serializedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].keywords.$Seen", equalTo(true));
+    }
+
+    @Test
+    public void setMessagesShouldReturnAnErrorWhenKeywordsWithAddingDraftArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.ANSWERED));
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Draft\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("keywords"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", equalTo("keywords: Cannot add or remove draft flag"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldReturnAnErrorWhenKeywordsWithDeletedArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.ANSWERED));
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Answered\": true, \"$Deleted\" : true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", containsString("Does not allow to update 'Deleted' or 'Recent' flag"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldReturnAnErrorWhenKeywordsWithRecentArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.ANSWERED));
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Answered\": true, \"$Recent\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", containsString("Does not allow to update 'Deleted' or 'Recent' flag"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldNotChangeOriginDeletedFlag() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.DELETED));
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Answered\": true, \"$Forwarded\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(messageId));
+
+        List<MessageResult> messages = messageProbe.getMessages(message.getMessageId(), USERNAME);
+        Flags expectedFlags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DELETED)
+            .add(FORWARDED)
+            .build();
+
+        assertThat(messages)
+            .hasSize(1)
+            .extracting(MessageResult::getFlags)
+            .containsOnly(expectedFlags);
+    }
+
+    @Test
+    public void setMessagesShouldNotChangeOriginRecentFlag() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+        Flags flags = FlagsBuilder.builder()
+            .add(Flag.DELETED, Flag.RECENT)
+            .build();
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, flags);
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Answered\": true, \"$Forwarded\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(messageId));
+
+        List<MessageResult> messages = messageProbe.getMessages(message.getMessageId(), USERNAME);
+
+        Flags expectedFlags = FlagsBuilder.builder()
+            .add(Flag.ANSWERED, Flag.DELETED, Flag.RECENT)
+            .add(FORWARDED)
+            .build();
+
+        assertThat(messages)
+            .hasSize(1)
+            .extracting(MessageResult::getFlags)
+            .containsOnly(expectedFlags);
+    }
+
+    @Test
+    public void setMessagesShouldReturnAnErrorWhenKeywordsWithRemoveDraftArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.DRAFT));
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("keywords"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", equalTo("keywords: Cannot add or remove draft flag"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldReturnNewKeywordsWhenKeywordsArePassedToRemoveAndAddFlag() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        Flags currentFlags = FlagsBuilder.builder()
+                .add(Flag.DRAFT, Flag.ANSWERED)
+                .build();
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, currentFlags);
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Draft\": true, \"$Flagged\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(messageId));
+
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + messageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].keywords.$Draft", equalTo(true))
+            .body(ARGUMENTS + ".list[0].keywords.$Flagged", equalTo(true));
+    }
+
     private ResponseSpecification getSetMessagesUpdateOKResponseAssertions(String messageId) {
         ResponseSpecBuilder builder = new ResponseSpecBuilder()
             .expectStatusCode(200)
@@ -656,7 +979,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldReturnNotFoundWhenUpdateUnknownMessage() {
+    public void setMessagesShouldReturnNotFoundWhenUpdateUnknownMessage() {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
 
         String nonExistingMessageId = randomMessageId().serialize();
@@ -677,7 +1000,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldReturnCreatedMessageWhenSendingMessage() {
+    public void setMessagesShouldReturnCreatedMessageWhenSendingMessage() {
         String messageCreationId = "creationId1337";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -717,10 +1040,10 @@ public abstract class SetMessagesMethodTest {
                 hasEntry(equalTo("threadId"), not(isEmptyOrNullString())),
                 hasEntry(equalTo("size"), not(isEmptyOrNullString()))
             )))
-            // assert that message flags are all unset
+            // assert that message FLAGS are all unset
             .body(ARGUMENTS + ".created", hasEntry(equalTo(messageCreationId), Matchers.allOf(
                 hasEntry(equalTo("isDraft"), equalTo(false)),
-                hasEntry(equalTo("isUnread"), equalTo(false)),
+                hasEntry(equalTo("isUnread"), equalTo(true)),
                 hasEntry(equalTo("isFlagged"), equalTo(false)),
                 hasEntry(equalTo("isAnswered"), equalTo(false))
             )))
@@ -728,7 +1051,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldReturnCreatedMessageWithEmptySubjectWhenSubjectIsNull() {
+    public void setMessagesShouldReturnCreatedMessageWithEmptySubjectWhenSubjectIsNull() {
         String messageCreationId = "creationId1337";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -762,7 +1085,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldReturnCreatedMessageWithEmptySubjectWhenSubjectIsEmpty() {
+    public void setMessagesShouldReturnCreatedMessageWithEmptySubjectWhenSubjectIsEmpty() {
         String messageCreationId = "creationId1337";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -794,9 +1117,9 @@ public abstract class SetMessagesMethodTest {
             .body(ARGUMENTS + ".created", hasKey(messageCreationId))
             .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].subject", equalTo(""));
     }
-    
+
     @Test
-    public void setMessageShouldReturnCreatedMessageWithNonASCIICharactersInSubjectWhenPresent() {
+    public void setMessagesShouldReturnCreatedMessageWithNonASCIICharactersInSubjectWhenPresent() {
         String messageCreationId = "creationId1337";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -830,7 +1153,77 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldSupportArbitraryMessageId() {
+    public void setMessageWithCreatedMessageShouldReturnAnErrorWhenBothIsFlagAndKeywordsPresent() {
+        String messageCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+                "        \"subject\": \"subject\"," +
+                "        \"isDraft\": true," +
+                "        \"keywords\": {\"$Draft\": true}," +
+                "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("error"))
+            .body(ARGUMENTS + ".type", equalTo("invalidArguments"))
+            .body(ARGUMENTS + ".description", containsString("Does not support keyword and is* at the same time"));
+    }
+
+    @Test
+    public void setMessageWithCreatedMessageShouldSupportKeywordsForFlags() {
+        String messageCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+                "        \"subject\": \"subject\"," +
+                "        \"keywords\": {\"$Draft\": true, \"$Flagged\": true}," +
+                "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            .body(ARGUMENTS + ".created", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].keywords.$Draft", equalTo(true))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].keywords.$Flagged", equalTo(true))
+            ;
+    }
+    @Test
+    public void setMessagesShouldSupportArbitraryMessageId() {
         String messageCreationId = "1717fcd1-603e-44a5-b2a6-1234dbcd5723";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -1325,7 +1718,7 @@ public abstract class SetMessagesMethodTest {
             .log().ifValidationFails()
             .statusCode(200)
             .body(SECOND_NAME, equalTo("messages"))
-            .body(SECOND_ARGUMENTS + ".list", hasSize(1)) 
+            .body(SECOND_ARGUMENTS + ".list", hasSize(1))
             .body(SECOND_ARGUMENTS + ".list[0].bcc", empty());
     }
 
@@ -1380,7 +1773,7 @@ public abstract class SetMessagesMethodTest {
             .log().ifValidationFails()
             .statusCode(200)
             .body(SECOND_NAME, equalTo("messages"))
-            .body(SECOND_ARGUMENTS + ".list", hasSize(1)) 
+            .body(SECOND_ARGUMENTS + ".list", hasSize(1))
             .body(SECOND_ARGUMENTS + ".list[0].bcc", hasSize(1));
     }
 
@@ -1439,7 +1832,7 @@ public abstract class SetMessagesMethodTest {
             .log().ifValidationFails()
             .statusCode(200)
             .body(SECOND_NAME, equalTo("messages"))
-            .body(SECOND_ARGUMENTS + ".list", hasSize(1)) 
+            .body(SECOND_ARGUMENTS + ".list", hasSize(1))
             .body(SECOND_ARGUMENTS + ".list[0].bcc", empty());
     }
 
@@ -1455,7 +1848,7 @@ public abstract class SetMessagesMethodTest {
                 .body(NAME, equalTo("messageList"))
                 .body(ARGUMENTS + ".messageIds", hasSize(1));
             return true;
-            
+
         } catch (AssertionError e) {
             return false;
         }
@@ -1591,7 +1984,7 @@ public abstract class SetMessagesMethodTest {
             .body(ARGUMENTS + ".created", aMapWithSize(0));
     }
 
-    
+
     private boolean isHtmlMessageReceived(AccessToken recipientToken) {
         try {
             with()
@@ -1609,7 +2002,7 @@ public abstract class SetMessagesMethodTest {
             return false;
         }
     }
-    
+
     @Test
     public void setMessagesShouldSendAReadableTextPlusHtmlMessage() throws Exception {
         // Recipient
@@ -2152,7 +2545,7 @@ public abstract class SetMessagesMethodTest {
             .body(ARGUMENTS + ".list", hasSize(1))
             .body(firstMessage + ".mailboxIds", containsInAnyOrder(trashId, mailboxId));
     }
-    
+
     @Test
     public void setMessagesShouldReturnAttachmentsNotFoundWhenBlobIdDoesntExist() throws Exception {
         String messageCreationId = "creationId";
@@ -2430,7 +2823,7 @@ public abstract class SetMessagesMethodTest {
         String thirdAttachment = message + ".attachments[2]";
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2438,7 +2831,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         given()
             .header("Authorization", accessToken.serialize())
             .body("[[\"getMessages\", {\"ids\": [\"" + receivedMessageId + "\"]}, \"#0\"]]")
@@ -2479,7 +2872,7 @@ public abstract class SetMessagesMethodTest {
             0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,
             50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,
             100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127};
-        
+
         Attachment attachment = Attachment.builder()
             .bytes(rawBytes)
             .type("application/octet-stream")
@@ -2521,7 +2914,7 @@ public abstract class SetMessagesMethodTest {
         calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2529,7 +2922,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         String firstMessage = ARGUMENTS + ".list[0]";
         String firstAttachment = firstMessage + ".attachments[0]";
         given()
@@ -2593,7 +2986,7 @@ public abstract class SetMessagesMethodTest {
         calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2601,7 +2994,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         String firstMessage = ARGUMENTS + ".list[0]";
         String firstAttachment = firstMessage + ".attachments[0]";
         given()
@@ -2634,7 +3027,7 @@ public abstract class SetMessagesMethodTest {
                 .body(NAME, equalTo("messageList"))
                 .body(ARGUMENTS + ".messageIds", hasSize(1));
             return true;
-            
+
         } catch (AssertionError e) {
             return false;
         }
@@ -2685,7 +3078,7 @@ public abstract class SetMessagesMethodTest {
         calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2693,7 +3086,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         String firstMessage = ARGUMENTS + ".list[0]";
         String firstAttachment = firstMessage + ".attachments[0]";
         given()
@@ -2758,7 +3151,7 @@ public abstract class SetMessagesMethodTest {
         calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2766,7 +3159,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         String firstMessage = ARGUMENTS + ".list[0]";
         String firstAttachment = firstMessage + ".attachments[0]";
         given()
@@ -2827,7 +3220,7 @@ public abstract class SetMessagesMethodTest {
         calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
 
         String inboxId = getMailboxId(accessToken, Role.INBOX);
-        String receivedMessageId = 
+        String receivedMessageId =
             with()
                 .header("Authorization", accessToken.serialize())
                 .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
@@ -2835,7 +3228,7 @@ public abstract class SetMessagesMethodTest {
             .then()
                 .extract()
                 .path(ARGUMENTS + ".messageIds[0]");
-        
+
         String firstMessage = ARGUMENTS + ".list[0]";
         String firstAttachment = firstMessage + ".attachments[0]";
         given()
@@ -2856,7 +3249,7 @@ public abstract class SetMessagesMethodTest {
             .body(firstAttachment + ".size", equalTo((int) attachment.getSize()));
     }
     @Test
-    public void setMessageShouldVerifyHeaderOfMessageInInbox() throws Exception {
+    public void setMessagesShouldVerifyHeaderOfMessageInInbox() throws Exception {
         String toUsername = "username1@" + USERS_DOMAIN;
         String password = "password";
         dataProbe.addUser(toUsername, password);
@@ -2893,7 +3286,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldVerifyHeaderOfMessageInSent() throws Exception {
+    public void setMessagesShouldVerifyHeaderOfMessageInSent() throws Exception {
         String toUsername = "username1@" + USERS_DOMAIN;
         String password = "password";
         dataProbe.addUser(toUsername, password);
@@ -2958,8 +3351,7 @@ public abstract class SetMessagesMethodTest {
                     .statusCode(200)
                     .body(ARGUMENTS + ".messageIds", hasSize(1))
                     .body(SECOND_NAME, equalTo("messages"))
-                    .body(SECOND_ARGUMENTS + ".list[0]", hasEntry(equalTo("headers"), allHeadersMatcher(expectedHeaders)))
-            ;
+                    .body(SECOND_ARGUMENTS + ".list[0]", hasEntry(equalTo("headers"), allHeadersMatcher(expectedHeaders)));
             return true;
         } catch(AssertionError e) {
             e.printStackTrace();
@@ -3210,4 +3602,91 @@ public abstract class SetMessagesMethodTest {
             .body(secondAttachment + ".cid", nullValue())
             .body(secondAttachment + ".isInline", equalTo(true));
     }
+
+    @Test
+    public void setMessageWithUpdateShouldBeOKWhenKeywordsWithCustomFlagArePassed() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
+
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, USER_MAILBOX,
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
+        await();
+
+        String messageId = message.getMessageId().serialize();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"keywords\": {\"$Seen\": true, \"$Unknown\": true} } } }, \"#0\"]]", messageId))
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .spec(getSetMessagesUpdateOKResponseAssertions(messageId));
+    }
+
+    @Test
+    public void setMessageWithCreationShouldBeOKWhenKeywordsWithCustomFlagArePassed() {
+        String messageCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+                "        \"subject\": \"subject\"," +
+                "        \"keywords\": {\"$Draft\": true, \"$Unknown\": true}," +
+                "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
+            .body(ARGUMENTS + ".created", aMapWithSize(1));
+    }
+
+    @Test
+    public void setMessageWithCreationShouldThrowWhenKeywordsWithUnsupportedArePassed() {
+        String messageCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String requestBody = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"create\": { \"" + messageCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+            "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+            "        \"subject\": \"subject\"," +
+            "        \"keywords\": {\"$Draft\": true, \"$Deleted\": true}," +
+            "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("error"))
+            .body(ARGUMENTS + ".type", equalTo("invalidArguments"))
+            .body(ARGUMENTS + ".description", containsString("Does not allow to update 'Deleted' or 'Recent' flag"));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[11/13] james-project git commit: JAMES-2046 Implement default use of internalDate if no SentDate on Lucene

Posted by bt...@apache.org.
JAMES-2046 Implement default use of internalDate if no SentDate on Lucene


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

Branch: refs/heads/master
Commit: df4d1d80c2a9211ac3aceb4978ab0683d5c42251
Parents: 21ce9e8
Author: benwa <bt...@linagora.com>
Authored: Thu Aug 24 14:06:26 2017 +0700
Committer: benwa <bt...@linagora.com>
Committed: Fri Aug 25 15:31:46 2017 +0700

----------------------------------------------------------------------
 .../james/mailbox/lucene/search/LuceneMessageSearchIndex.java     | 3 +++
 1 file changed, 3 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/df4d1d80/mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java
----------------------------------------------------------------------
diff --git a/mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java b/mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java
index 319ad7d..3227bef 100644
--- a/mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java
+++ b/mailbox/lucene/src/main/java/org/apache/james/mailbox/lucene/search/LuceneMessageSearchIndex.java
@@ -610,6 +610,9 @@ public class LuceneMessageSearchIndex extends ListeningMessageSearchIndex {
                             // This should never happen anyway fallback to the already parsed field
                             sentDate = ((DateTimeField) f).getDate();
                         }
+                        if (sentDate == null) {
+                            sentDate = membership.getInternalDate();
+                        }
 
                     } 
                         String field = null;


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[10/13] james-project git commit: JAMES-2046 Implement robust SentDate parsing for Memory search

Posted by bt...@apache.org.
JAMES-2046 Implement robust SentDate parsing for Memory search

Reuse ElasticSearch code.


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

Branch: refs/heads/master
Commit: 21ce9e82a5effac912841eede6fc54a09d7d0090
Parents: eaedcd2
Author: benwa <bt...@linagora.com>
Authored: Thu Aug 24 13:40:16 2017 +0700
Committer: benwa <bt...@linagora.com>
Committed: Fri Aug 25 15:31:42 2017 +0700

----------------------------------------------------------------------
 .../elasticsearch/json/HeaderCollection.java    | 42 +----------
 .../json/HeaderCollectionTest.java              | 28 --------
 .../search/comparator/CombinedComparator.java   | 13 ++--
 .../search/comparator/SentDateComparator.java   | 73 +++++++++++++-------
 .../comparator/SentDateComparatorTest.java      | 68 ++++++++++++++++++
 5 files changed, 124 insertions(+), 100 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/21ce9e82/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollection.java
----------------------------------------------------------------------
diff --git a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollection.java b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollection.java
index 2b26a66..85782ad 100644
--- a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollection.java
+++ b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollection.java
@@ -24,23 +24,18 @@ import java.util.HashSet;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.james.mailbox.store.search.SearchUtil;
+import org.apache.james.mailbox.store.search.comparator.SentDateComparator;
 import org.apache.james.mime4j.dom.address.Address;
 import org.apache.james.mime4j.dom.address.Group;
 import org.apache.james.mime4j.dom.address.Mailbox;
 import org.apache.james.mime4j.field.address.LenientAddressParser;
 import org.apache.james.mime4j.stream.Field;
 import org.apache.james.mime4j.util.MimeUtil;
-import org.apache.james.util.date.ImapDateTimeFormatter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMultimap;
@@ -51,14 +46,6 @@ public class HeaderCollection {
 
     public static class Builder {
 
-        // Some sent e-mail have this form : Wed,  3 Jun 2015 09:05:46 +0000 (UTC)
-        // Java 8 Time library RFC_1123_DATE_TIME corresponds to Wed,  3 Jun 2015 09:05:46 +0000 only
-        // This REGEXP is here to match ( in order to remove ) the possible invalid end of a header date
-        // Example of matching patterns :
-        //  (UTC)
-        //  (CEST)
-        private static final Pattern DATE_SANITIZING_PATTERN = Pattern.compile(" *\\(.*\\) *");
-
         private final Set<EMailer> toAddressSet;
         private final Set<EMailer> fromAddressSet;
         private final Set<EMailer> ccAddressSet;
@@ -114,7 +101,7 @@ public class HeaderCollection {
                     subjectSet.add(headerValue);
                     break;
                 case DATE:
-                    sentDate = toISODate(headerValue);
+                    sentDate = SentDateComparator.toISODate(headerValue);
                     break;
             }
         }
@@ -152,29 +139,6 @@ public class HeaderCollection {
             }
             throw new RuntimeException(headerName + " is not a address header name");
         }
-
-        private Optional<ZonedDateTime> toISODate(String value) {
-            try {
-                return Optional.of(ZonedDateTime.parse(
-                    sanitizeDateStringHeaderValue(value),
-                    ImapDateTimeFormatter.rfc5322()));
-            } catch (Exception e) {
-                LOGGER.info("Can not parse receive date " + value);
-                return Optional.empty();
-            }
-        }
-
-        @VisibleForTesting String sanitizeDateStringHeaderValue(String value) {
-            // Some sent e-mail have this form : Wed,  3 Jun 2015 09:05:46 +0000 (UTC)
-            // Java 8 Time library RFC_1123_DATE_TIME corresponds to Wed,  3 Jun 2015 09:05:46 +0000 only
-            // This method is here to convert the first date into something parsable by RFC_1123_DATE_TIME DateTimeFormatter
-            Matcher sanitizerMatcher = DATE_SANITIZING_PATTERN.matcher(value);
-            if (sanitizerMatcher.find()) {
-                return value.substring(0 , sanitizerMatcher.start());
-            }
-            return value;
-        }
-
     }
 
     public static final String TO = "to";
@@ -185,8 +149,6 @@ public class HeaderCollection {
     public static final String SUBJECT = "subject";
     public static final String DATE = "date";
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(HeaderCollection.class);
-
     public static Builder builder() {
         return new Builder();
     }

http://git-wip-us.apache.org/repos/asf/james-project/blob/21ce9e82/mailbox/elasticsearch/src/test/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollectionTest.java
----------------------------------------------------------------------
diff --git a/mailbox/elasticsearch/src/test/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollectionTest.java b/mailbox/elasticsearch/src/test/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollectionTest.java
index b1f8fae..93a7b02 100644
--- a/mailbox/elasticsearch/src/test/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollectionTest.java
+++ b/mailbox/elasticsearch/src/test/java/org/apache/james/mailbox/elasticsearch/json/HeaderCollectionTest.java
@@ -267,32 +267,4 @@ public class HeaderCollectionTest {
         HeaderCollection.builder().add(null).build();
     }
 
-    @Test
-    public void sanitizeDateStringHeaderValueShouldRemoveCESTPart() {
-        assertThat(HeaderCollection.builder()
-            .sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200 (CEST)"))
-            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
-    }
-
-    @Test
-    public void sanitizeDateStringHeaderValueShouldRemoveUTCPart() {
-        assertThat(HeaderCollection.builder()
-            .sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200  (UTC)  "))
-            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
-    }
-
-    @Test
-    public void sanitizeDateStringHeaderValueShouldNotChangeAcceptableString() {
-        assertThat(HeaderCollection.builder()
-            .sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200"))
-            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
-    }
-
-    @Test
-    public void sanitizeDateStringHeaderValueShouldNotChangeEmptyString() {
-        assertThat(HeaderCollection.builder()
-            .sanitizeDateStringHeaderValue(""))
-            .isEqualTo("");
-    }
-
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/21ce9e82/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/CombinedComparator.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/CombinedComparator.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/CombinedComparator.java
index 38bae1d..53d270f 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/CombinedComparator.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/CombinedComparator.java
@@ -95,14 +95,11 @@ public class CombinedComparator implements Comparator<MailboxMessage>{
 
     @Override
     public int compare(MailboxMessage o1, MailboxMessage o2) {
-        int i = 0;
-        for (Comparator<MailboxMessage> comparator : comparators) {
-            i = comparator.compare(o1, o2);
-            if (i != 0) {
-                break;
-            }
-        }
-        return i;
+        return comparators.stream()
+            .map(comparator -> comparator.compare(o1, o2))
+            .filter(result -> result != 0)
+            .findFirst()
+            .orElse(0);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/21ce9e82/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/SentDateComparator.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/SentDateComparator.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/SentDateComparator.java
index 8a1373e..3a897f7 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/SentDateComparator.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/comparator/SentDateComparator.java
@@ -18,44 +18,69 @@
  ****************************************************************/
 package org.apache.james.mailbox.store.search.comparator;
 
-import java.io.StringReader;
+import java.time.Instant;
+import java.time.ZonedDateTime;
 import java.util.Comparator;
-import java.util.Date;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
-import org.apache.james.mime4j.dom.datetime.DateTime;
-import org.apache.james.mime4j.field.datetime.parser.DateTimeParser;
-import org.apache.james.mime4j.field.datetime.parser.ParseException;
+import org.apache.james.util.date.ImapDateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
 
 /**
  * {@link Comparator} which works like stated in RFC5256 2.2 Sent Date
- *
  */
 public class SentDateComparator extends AbstractHeaderComparator {
+
     public final static Comparator<MailboxMessage> SENTDATE = new SentDateComparator();
+    private static final Logger LOGGER = LoggerFactory.getLogger(SentDateComparator.class);
+    // Some sent e-mail have this form : Wed,  3 Jun 2015 09:05:46 +0000 (UTC)
+    // Java 8 Time library RFC_1123_DATE_TIME corresponds to Wed,  3 Jun 2015 09:05:46 +0000 only
+    // This REGEXP is here to match ( in order to remove ) the possible invalid end of a header date
+    // Example of matching patterns :
+    //  (UTC)
+    //  (CEST)
+    private static final Pattern DATE_SANITIZING_PATTERN = Pattern.compile(" *\\(.*\\) *");
+
+    public static Optional<ZonedDateTime> toISODate(String value) {
+        try {
+            return Optional.of(ZonedDateTime.parse(
+                sanitizeDateStringHeaderValue(value),
+                ImapDateTimeFormatter.rfc5322()));
+        } catch (Exception e) {
+            LOGGER.info("Can not parse receive date " + value);
+            return Optional.empty();
+        }
+    }
+
+     @VisibleForTesting
+     static String sanitizeDateStringHeaderValue(String value) {
+        // Some sent e-mail have this form : Wed,  3 Jun 2015 09:05:46 +0000 (UTC)
+        // Java 8 Time library RFC_1123_DATE_TIME corresponds to Wed,  3 Jun 2015 09:05:46 +0000 only
+        // This method is here to convert the first date into something parsable by RFC_1123_DATE_TIME DateTimeFormatter
+        Matcher sanitizerMatcher = DATE_SANITIZING_PATTERN.matcher(value);
+        if (sanitizerMatcher.find()) {
+            return value.substring(0 , sanitizerMatcher.start());
+        }
+        return value;
+    }
 
     @Override
     public int compare(MailboxMessage o1, MailboxMessage o2) {
-        Date date1 = getSentDate(o1);
-        Date date2 = getSentDate(o2);
-        int i = date1.compareTo(date2);
-        
-        // sent date was the same so use the uid as tie-breaker
-        if (i == 0) {
-            return UidComparator.UID.compare(o1, o2);
-        }
-        return 0;
+        Instant date1 = getSentDate(o1);
+        Instant date2 = getSentDate(o2);
+        return date1.compareTo(date2);
     }
     
-    private Date getSentDate(MailboxMessage message) {
+    private Instant getSentDate(MailboxMessage message) {
         final String value = getHeaderValue("Date", message);
-        final StringReader reader = new StringReader(value);
-        try {
-            DateTime dateTime = new DateTimeParser(reader).parseAll();
-            return dateTime.getDate();
-        } catch (ParseException e) {
-            // if we can not parse the date header we should use the internaldate as fallback
-            return message.getInternalDate();
-        }
+        return toISODate(value)
+            .map(ZonedDateTime::toInstant)
+            .orElse(message.getInternalDate().toInstant());
     }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/21ce9e82/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/comparator/SentDateComparatorTest.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/comparator/SentDateComparatorTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/comparator/SentDateComparatorTest.java
new file mode 100644
index 0000000..ddd5a63
--- /dev/null
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/comparator/SentDateComparatorTest.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.mailbox.store.search.comparator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+public class SentDateComparatorTest {
+    @Test
+    public void sanitizeDateStringHeaderValueShouldRemoveCESTPart() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200 (CEST)"))
+            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
+    }
+
+    @Test
+    public void sanitizeDateStringHeaderValueShouldRemoveUTCPart() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200  (UTC)  "))
+            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
+    }
+
+    @Test
+    public void sanitizeDateStringHeaderValueShouldNotChangeAcceptableString() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue("Thu, 18 Jun 2015 04:09:35 +0200"))
+            .isEqualTo("Thu, 18 Jun 2015 04:09:35 +0200");
+    }
+
+    @Test
+    public void sanitizeDateStringHeaderValueShouldRemoveBrackets() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue("invalid (removeMe)"))
+            .isEqualTo("invalid");
+    }
+
+    @Test
+    public void sanitizeDateStringHeaderValueShouldKeepUnclosedBrackets() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue("invalid (removeMe"))
+            .isEqualTo("invalid (removeMe");
+    }
+
+    @Test
+    public void sanitizeDateStringHeaderValueShouldNotChangeEmptyString() {
+        assertThat(
+            SentDateComparator.sanitizeDateStringHeaderValue(""))
+            .isEqualTo("");
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[13/13] james-project git commit: JAMES-2046 integration test for GetMessageList date ordering without sent date

Posted by bt...@apache.org.
JAMES-2046 integration test for GetMessageList date ordering without sent date


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

Branch: refs/heads/master
Commit: 10d76ab6eed59eb5cf5ae3d18772a33437d82053
Parents: 5af0d27
Author: benwa <bt...@linagora.com>
Authored: Thu Aug 24 13:11:12 2017 +0700
Committer: benwa <bt...@linagora.com>
Committed: Fri Aug 25 15:31:51 2017 +0700

----------------------------------------------------------------------
 .../integration/GetMessageListMethodTest.java   | 23 ++++++++++++++++++++
 1 file changed, 23 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/10d76ab6/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
index 38ef074..df53f63 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
@@ -1582,6 +1582,29 @@ public abstract class GetMessageListMethodTest {
                 containsInAnyOrder(messageFlagged.getMessageId().serialize(), messageNotFlagged.getMessageId().serialize())));
     }
 
+    @Test
+    public void getMessageListShouldSortUsingInternalDateWhenNoDateHeader() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        LocalDate date = LocalDate.now();
+        ComposedMessageId message1 = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), convertToDate(date.plusDays(1)), false, new Flags());
+        ComposedMessageId message2 = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes()), convertToDate(date), false, new Flags());
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"sort\":[\"date asc\"]}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", contains(message2.getMessageId().serialize(), message1.getMessageId().serialize()));
+    }
+
     private Date convertToDate(LocalDate localDate) {
         return Date.from(localDate.atStartOfDay(ZONE_ID).toInstant());
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[05/13] james-project git commit: JAMES-2110 SetMessage with creation should support keywords for flags

Posted by bt...@apache.org.
JAMES-2110 SetMessage with creation should support keywords for flags


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

Branch: refs/heads/master
Commit: 78a4a57de45038e63011e867e9a46419540cb4e5
Parents: c1387ef
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:42:17 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:27 2017 +0200

----------------------------------------------------------------------
 .../methods/SetMessagesCreationProcessor.java   | 30 +++-----
 .../james/jmap/model/CreationMessage.java       | 73 ++++++++++----------
 .../SetMessagesCreationProcessorTest.java       | 57 ++++++++++++++-
 3 files changed, 101 insertions(+), 59 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/78a4a57d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
index 34bd029..bb9f189 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
@@ -20,14 +20,12 @@
 package org.apache.james.jmap.methods;
 
 import static org.apache.james.jmap.methods.Method.JMAP_PREFIX;
-
 import java.io.IOException;
 import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
-
 import javax.inject.Inject;
 import javax.mail.Flags;
 import javax.mail.MessagingException;
@@ -40,6 +38,7 @@ import org.apache.james.jmap.model.Attachment;
 import org.apache.james.jmap.model.BlobId;
 import org.apache.james.jmap.model.CreationMessage;
 import org.apache.james.jmap.model.CreationMessageId;
+import org.apache.james.jmap.model.Keywords;
 import org.apache.james.jmap.model.Message;
 import org.apache.james.jmap.model.MessageFactory;
 import org.apache.james.jmap.model.MessageFactory.MetaDataWithContent;
@@ -70,6 +69,7 @@ import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.metrics.api.TimeMetric;
 import org.apache.james.util.OptionalConverter;
 import org.apache.mailet.Mail;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -82,6 +82,7 @@ import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 
+
 public class SetMessagesCreationProcessor implements SetMessagesProcessor {
 
     private static final Logger LOG = LoggerFactory.getLogger(SetMailboxesCreationProcessor.class);
@@ -290,16 +291,20 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         byte[] messageContent = mimeMessageConverter.convert(createdEntry, messageAttachments);
         SharedByteArrayInputStream content = new SharedByteArrayInputStream(messageContent);
         Date internalDate = Date.from(createdEntry.getValue().getDate().toInstant());
-        Flags flags = getMessageFlags(createdEntry.getValue());
+
+        Flags flags = createdEntry.getValue()
+            .getKeywords()
+            .map(Keywords::asFlags)
+            .orElse(new Flags());
 
         ComposedMessageId message = outbox.appendMessage(content, internalDate, session, flags.contains(Flags.Flag.RECENT), flags);
 
         return MetaDataWithContent.builder()
                 .uid(message.getUid())
                 .flags(flags)
-                .size(messageContent.length)
                 .internalDate(internalDate.toInstant())
                 .sharedContent(content)
+                .size(messageContent.length)
                 .attachments(messageAttachments)
                 .mailboxId(outbox.getId())
                 .messageId(message.getMessageId())
@@ -332,23 +337,6 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         }
     }
 
-    private Flags getMessageFlags(CreationMessage message) {
-        Flags result = new Flags();
-        if (!message.isIsUnread()) {
-            result.add(Flags.Flag.SEEN);
-        }
-        if (message.isIsFlagged()) {
-            result.add(Flags.Flag.FLAGGED);
-        }
-        if (message.isIsAnswered() || message.getInReplyToMessageId().isPresent()) {
-            result.add(Flags.Flag.ANSWERED);
-        }
-        if (message.isIsDraft()) {
-            result.add(Flags.Flag.DRAFT);
-        }
-        return result;
-    }
-
     private MessageWithId sendMessage(CreationMessageId creationId, MetaDataWithContent message, MailboxSession session) throws MailboxException, MessagingException {
         Message jmapMessage = messageFactory.fromMetaDataWithContent(message);
         sendMessage(message, jmapMessage, session);

http://git-wip-us.apache.org/repos/asf/james-project/blob/78a4a57d/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
index 791b383..f67185d 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
@@ -27,7 +27,6 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-
 import javax.mail.internet.AddressException;
 import javax.mail.internet.InternetAddress;
 
@@ -59,10 +58,10 @@ public class CreationMessage {
     public static class Builder {
         private ImmutableList<String> mailboxIds;
         private String inReplyToMessageId;
-        private boolean isUnread;
-        private boolean isFlagged;
-        private boolean isAnswered;
-        private boolean isDraft;
+        private Optional<Boolean> isUnread = Optional.empty();
+        private Optional<Boolean> isFlagged = Optional.empty();
+        private Optional<Boolean> isAnswered = Optional.empty();
+        private Optional<Boolean> isDraft = Optional.empty();
         private final ImmutableMap.Builder<String, String> headers;
         private Optional<DraftEmailer> from = Optional.empty();
         private final ImmutableList.Builder<DraftEmailer> to;
@@ -75,6 +74,7 @@ public class CreationMessage {
         private String htmlBody;
         private final ImmutableList.Builder<Attachment> attachments;
         private final ImmutableMap.Builder<BlobId, SubMessage> attachedMessages;
+        private Optional<Map<String, Boolean>> keywords = Optional.empty();
 
         private Builder() {
             to = ImmutableList.builder();
@@ -101,22 +101,22 @@ public class CreationMessage {
             return this;
         }
 
-        public Builder isUnread(boolean isUnread) {
+        public Builder isUnread(Optional<Boolean> isUnread) {
             this.isUnread = isUnread;
             return this;
         }
 
-        public Builder isFlagged(boolean isFlagged) {
+        public Builder isFlagged(Optional<Boolean> isFlagged) {
             this.isFlagged = isFlagged;
             return this;
         }
 
-        public Builder isAnswered(boolean isAnswered) {
+        public Builder isAnswered(Optional<Boolean> isAnswered) {
             this.isAnswered = isAnswered;
             return this;
         }
 
-        public Builder isDraft(boolean isDraft) {
+        public Builder isDraft(Optional<Boolean> isDraft) {
             this.isDraft = isDraft;
             return this;
         }
@@ -186,6 +186,11 @@ public class CreationMessage {
             return this;
         }
 
+        public Builder keywords(Map<String, Boolean> keywords) {
+            this.keywords = Optional.of(ImmutableMap.copyOf(keywords));
+            return this;
+        }
+
         private static boolean areAttachedMessagesKeysInAttachments(ImmutableList<Attachment> attachments, ImmutableMap<BlobId, SubMessage> attachedMessages) {
             return attachedMessages.isEmpty() || attachedMessages.keySet().stream()
                     .anyMatch(inAttachments(attachments));
@@ -206,21 +211,29 @@ public class CreationMessage {
             ImmutableMap<BlobId, SubMessage> attachedMessages = this.attachedMessages.build();
             Preconditions.checkState(areAttachedMessagesKeysInAttachments(attachments, attachedMessages), "'attachedMessages' keys must be in 'attachments'");
 
+            Optional<Keywords> creationKeywords = Keywords.factory()
+                .throwOnImapNonExposedKeywords()
+                .fromMapOrOldKeyword(keywords, getOldKeywords());
+
             if (date == null) {
                 date = ZonedDateTime.now();
             }
 
-            return new CreationMessage(mailboxIds, Optional.ofNullable(inReplyToMessageId), isUnread, isFlagged, isAnswered, isDraft, headers.build(), from,
-                    to.build(), cc.build(), bcc.build(), replyTo.build(), subject, date, Optional.ofNullable(textBody), Optional.ofNullable(htmlBody), attachments, attachedMessages);
+            return new CreationMessage(mailboxIds, Optional.ofNullable(inReplyToMessageId), headers.build(), from,
+                    to.build(), cc.build(), bcc.build(), replyTo.build(), subject, date, Optional.ofNullable(textBody), Optional.ofNullable(htmlBody),
+                    attachments, attachedMessages, creationKeywords);
+        }
+
+        private Optional<OldKeyword> getOldKeywords() {
+            if (isAnswered.isPresent() || isFlagged.isPresent() || isUnread.isPresent() || isDraft.isPresent()) {
+                return Optional.of(new OldKeyword(isUnread, isFlagged, isAnswered, isDraft));
+            }
+            return Optional.empty();
         }
     }
 
     private final ImmutableList<String> mailboxIds;
     private final Optional<String> inReplyToMessageId;
-    private final boolean isUnread;
-    private final boolean isFlagged;
-    private final boolean isAnswered;
-    private final boolean isDraft;
     private final ImmutableMap<String, String> headers;
     private final Optional<DraftEmailer> from;
     private final ImmutableList<DraftEmailer> to;
@@ -233,17 +246,14 @@ public class CreationMessage {
     private final Optional<String> htmlBody;
     private final ImmutableList<Attachment> attachments;
     private final ImmutableMap<BlobId, SubMessage> attachedMessages;
+    private final Optional<Keywords> keywords;
 
     @VisibleForTesting
-    CreationMessage(ImmutableList<String> mailboxIds, Optional<String> inReplyToMessageId, boolean isUnread, boolean isFlagged, boolean isAnswered, boolean isDraft, ImmutableMap<String, String> headers, Optional<DraftEmailer> from,
+    CreationMessage(ImmutableList<String> mailboxIds, Optional<String> inReplyToMessageId, ImmutableMap<String, String> headers, Optional<DraftEmailer> from,
                     ImmutableList<DraftEmailer> to, ImmutableList<DraftEmailer> cc, ImmutableList<DraftEmailer> bcc, ImmutableList<DraftEmailer> replyTo, String subject, ZonedDateTime date, Optional<String> textBody, Optional<String> htmlBody, ImmutableList<Attachment> attachments,
-                    ImmutableMap<BlobId, SubMessage> attachedMessages) {
+                    ImmutableMap<BlobId, SubMessage> attachedMessages, Optional<Keywords> keywords) {
         this.mailboxIds = mailboxIds;
         this.inReplyToMessageId = inReplyToMessageId;
-        this.isUnread = isUnread;
-        this.isFlagged = isFlagged;
-        this.isAnswered = isAnswered;
-        this.isDraft = isDraft;
         this.headers = headers;
         this.from = from;
         this.to = to;
@@ -256,6 +266,7 @@ public class CreationMessage {
         this.htmlBody = htmlBody;
         this.attachments = attachments;
         this.attachedMessages = attachedMessages;
+        this.keywords = keywords;
     }
 
     public ImmutableList<String> getMailboxIds() {
@@ -266,22 +277,6 @@ public class CreationMessage {
         return inReplyToMessageId;
     }
 
-    public boolean isIsUnread() {
-        return isUnread;
-    }
-
-    public boolean isIsFlagged() {
-        return isFlagged;
-    }
-
-    public boolean isIsAnswered() {
-        return isAnswered;
-    }
-
-    public boolean isIsDraft() {
-        return isDraft;
-    }
-
     public ImmutableMap<String, String> getHeaders() {
         return headers;
     }
@@ -334,6 +329,10 @@ public class CreationMessage {
         return validate().isEmpty();
     }
 
+    public Optional<Keywords> getKeywords() {
+        return keywords;
+    }
+
     public List<ValidationResult> validate() {
         ImmutableList.Builder<ValidationResult> errors = ImmutableList.builder();
         assertValidFromProvided(errors);

http://git-wip-us.apache.org/repos/asf/james-project/blob/78a4a57d/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
index cc10186..c2c36e6 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
@@ -42,6 +42,7 @@ import org.apache.james.jmap.model.BlobId;
 import org.apache.james.jmap.model.CreationMessage;
 import org.apache.james.jmap.model.CreationMessage.DraftEmailer;
 import org.apache.james.jmap.model.CreationMessageId;
+import org.apache.james.jmap.model.Keyword;
 import org.apache.james.jmap.model.MessageFactory;
 import org.apache.james.jmap.model.MessagePreviewGenerator;
 import org.apache.james.jmap.model.MessageProperties.MessageProperty;
@@ -72,9 +73,12 @@ import org.apache.james.util.mime.MessageContentExtractor;
 import org.apache.mailet.Mail;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 public class SetMessagesCreationProcessorTest {
@@ -115,7 +119,8 @@ public class SetMessagesCreationProcessorTest {
     private Optional<MessageManager> optionalOutbox;
     private Optional<MessageManager> optionalDrafts;
 
-
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
     @Before
     public void setUp() throws MailboxException {
         HtmlTextExtractor htmlTextExtractor = mock(HtmlTextExtractor.class);
@@ -157,6 +162,56 @@ public class SetMessagesCreationProcessorTest {
     }
 
     @Test
+    public void processShouldThrowWhenBothIsFlagAndKeywords() {
+        expectedException.expect(IllegalArgumentException.class);
+        SetMessagesRequest createMessageWithError = SetMessagesRequest.builder()
+            .create(
+                creationMessageId,
+                creationMessageBuilder
+                    .mailboxId(OUTBOX_ID.serialize())
+                    .isAnswered(Optional.of(true))
+                    .keywords(ImmutableMap.of("$Answered", true))
+                    .build())
+            .build();
+
+        sut.process(createMessageWithError, session);
+    }
+
+    @Test
+    public void processShouldCreateWhenKeywords() {
+        SetMessagesRequest createMessageWithKeywords = SetMessagesRequest.builder()
+            .create(
+                creationMessageId,
+                creationMessageBuilder
+                    .mailboxId(OUTBOX_ID.serialize())
+                    .keywords(ImmutableMap.of("$Answered", true))
+                    .build())
+            .build();
+
+        SetMessagesResponse result = sut.process(createMessageWithKeywords, session);
+
+        assertThat(result.getCreated()).isNotEmpty();
+        assertThat(result.getNotCreated()).isEmpty();
+    }
+
+    @Test
+    public void processShouldCreateWhenIsFlag() {
+        SetMessagesRequest createMessageWithKeywords = SetMessagesRequest.builder()
+            .create(
+                creationMessageId,
+                creationMessageBuilder
+                    .mailboxId(OUTBOX_ID.serialize())
+                    .isAnswered(Optional.of(true))
+                    .build())
+            .build();
+
+        SetMessagesResponse result = sut.process(createMessageWithKeywords, session);
+
+        assertThat(result.getCreated()).isNotEmpty();
+        assertThat(result.getNotCreated()).isEmpty();
+    }
+
+    @Test
     public void processShouldReturnNonEmptyCreatedWhenRequestHasNonEmptyCreate() throws MailboxException {
         // Given
         sut = new SetMessagesCreationProcessor(mimeMessageConverter, mockedMailSpool, mockedMailFactory, messageFactory, fakeSystemMailboxesProvider, mockedAttachmentManager, new NoopMetricFactory());


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[03/13] james-project git commit: JAMES-2110 Parser message should not care json message deserilization such as we had CreationMessage and UpdateMessagePath

Posted by bt...@apache.org.
JAMES-2110 Parser message should not care json message deserilization such as we had CreationMessage and UpdateMessagePath


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

Branch: refs/heads/master
Commit: 53fdcbdd86d35e55d075fa0ad79ebd69b11ec735
Parents: 20beee7
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:35:50 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:27 2017 +0200

----------------------------------------------------------------------
 .../james/jmap/json/ParsingWritingObjects.java    | 18 ++++++++++--------
 .../jmap/json/ParsingWritingObjectsTest.java      | 10 ----------
 .../jmap/src/test/resources/json/message.json     |  5 +++--
 3 files changed, 13 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/53fdcbdd/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjects.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjects.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjects.java
index 144e144..f84b91e 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjects.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjects.java
@@ -22,10 +22,14 @@ package org.apache.james.jmap.json;
 import java.time.Instant;
 import java.util.Optional;
 
+import javax.mail.Flags;
+
 import org.apache.james.jmap.model.BlobId;
 import org.apache.james.jmap.model.Emailer;
+import org.apache.james.jmap.model.Keyword;
 import org.apache.james.jmap.model.Message;
 import org.apache.james.jmap.model.SubMessage;
+import org.apache.james.mailbox.FlagsBuilder;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MessageId;
@@ -33,6 +37,7 @@ import org.apache.james.mailbox.model.TestMessageId;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 
 public interface ParsingWritingObjects {
 
@@ -42,10 +47,10 @@ public interface ParsingWritingObjects {
         String THREAD_ID = "myThreadId";
         ImmutableList<MailboxId> MAILBOX_IDS = ImmutableList.of(InMemoryId.of(1), InMemoryId.of(2));
         String IN_REPLY_TO_MESSAGE_ID = "myInReplyToMessageId";
-        boolean IS_UNREAD = true;
-        boolean IS_FLAGGED = true;
-        boolean IS_ANSWERED = true;
-        boolean IS_DRAFT = true;
+        Flags FLAGS = FlagsBuilder.builder()
+                .add(Flags.Flag.FLAGGED, Flags.Flag.ANSWERED, Flags.Flag.DRAFT, Flags.Flag.SEEN)
+                .build();
+        ImmutableSet<Keyword> KEYWORDS = ImmutableSet.of(Keyword.DRAFT, Keyword.FLAGGED, Keyword.ANSWERED, Keyword.SEEN);
         boolean HAS_ATTACHMENT = true;
         ImmutableMap<String, String> HEADERS = ImmutableMap.of("h1", "h1Value", "h2", "h2Value");
         Emailer FROM = Emailer.builder().name("myName").email("myEmail@james.org").build();
@@ -71,10 +76,7 @@ public interface ParsingWritingObjects {
             .threadId(Common.THREAD_ID)
             .mailboxIds(Common.MAILBOX_IDS)
             .inReplyToMessageId(Common.IN_REPLY_TO_MESSAGE_ID)
-            .isUnread(Common.IS_UNREAD)
-            .isFlagged(Common.IS_FLAGGED)
-            .isAnswered(Common.IS_ANSWERED)
-            .isDraft(Common.IS_DRAFT)
+            .flags(Common.FLAGS)
             .headers(Common.HEADERS)
             .from(Common.FROM)
             .to(Common.TO)

http://git-wip-us.apache.org/repos/asf/james-project/blob/53fdcbdd/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjectsTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjectsTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjectsTest.java
index 0cd3a1f..bed119d 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjectsTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/json/ParsingWritingObjectsTest.java
@@ -73,16 +73,6 @@ public class ParsingWritingObjectsTest {
     }
 
     @Test
-    public void parsingJsonShouldWorkOnMessage() throws Exception {
-        Message expected = MESSAGE;
-
-        Message message = testee.forParsing()
-            .readValue(IOUtils.toString(ClassLoader.getSystemResource("json/message.json"), StandardCharsets.UTF_8), Message.class);
-
-        assertThat(message).isEqualToComparingFieldByField(expected);
-    }
-
-    @Test
     public void writingJsonShouldWorkOnMessage() throws Exception {
         String expected = IOUtils.toString(ClassLoader.getSystemResource("json/message.json"), StandardCharsets.UTF_8);
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/53fdcbdd/server/protocols/jmap/src/test/resources/json/message.json
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/resources/json/message.json b/server/protocols/jmap/src/test/resources/json/message.json
index e036384..ba0c82f 100644
--- a/server/protocols/jmap/src/test/resources/json/message.json
+++ b/server/protocols/jmap/src/test/resources/json/message.json
@@ -4,7 +4,7 @@
     "threadId": "myThreadId",
     "mailboxIds": ["1", "2"],
     "inReplyToMessageId": "myInReplyToMessageId",
-    "isUnread": true,
+    "isUnread": false,
     "isFlagged": true,
     "isAnswered": true,
     "isDraft": true,
@@ -22,5 +22,6 @@
     "textBody": "myTextBody",
     "htmlBody": "<h1>myHtmlBody</h1>",
     "attachments": [ ],
-    "attachedMessages": { }
+    "attachedMessages": { },
+    "keywords": {"$Draft" : true, "$Flagged" : true, "$Answered" : true, "$Seen" : true}
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[09/13] james-project git commit: JAMES-2110 IT for getMessageList which supports filter by hasKeyword and notKeyword

Posted by bt...@apache.org.
JAMES-2110 IT for getMessageList which supports filter by hasKeyword and notKeyword


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

Branch: refs/heads/master
Commit: eaedcd2182fbc39578f81eaf4a1ab07983ec6e06
Parents: 41e3d15
Author: quynhn <qn...@linagora.com>
Authored: Tue Aug 15 16:49:13 2017 +0700
Committer: Raphael Ouazana <ra...@linagora.com>
Committed: Thu Aug 24 15:47:28 2017 +0200

----------------------------------------------------------------------
 .../integration/GetMessageListMethodTest.java   | 286 +++++++++++++++++--
 1 file changed, 268 insertions(+), 18 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/eaedcd21/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
index 06bf60b..38ef074 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/GetMessageListMethodTest.java
@@ -29,15 +29,12 @@ import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.not;
-
 import java.io.ByteArrayInputStream;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.Date;
-
 import javax.mail.Flags;
 
-import org.apache.http.client.utils.URIBuilder;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.jmap.HttpJmapAuthentication;
 import org.apache.james.jmap.api.access.AccessToken;
@@ -53,16 +50,19 @@ import org.apache.james.probe.DataProbe;
 import org.apache.james.util.date.ImapDateTimeFormatter;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.JmapGuiceProbe;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.apache.http.client.utils.URIBuilder;
 
 import com.google.common.base.Charsets;
 import com.jayway.restassured.RestAssured;
 import com.jayway.restassured.builder.RequestSpecBuilder;
 import com.jayway.restassured.http.ContentType;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
 public abstract class GetMessageListMethodTest {
+    private final static String FORWARDED = "$Forwarded";
     private static final String NAME = "[0][0]";
     private static final String ARGUMENTS = "[0][1]";
     private static final ZoneId ZONE_ID = ZoneId.of("Europe/Paris");
@@ -140,7 +140,7 @@ public abstract class GetMessageListMethodTest {
     }
     
     @Test
-    public void getMessageListSetFlaggedFilterShouldWork() throws Exception {
+    public void getMessageListSetFlaggedFilterShouldResultFlaggedMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -164,7 +164,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListUnsetFlaggedFilterShouldWork() throws Exception {
+    public void getMessageListUnsetFlaggedFilterShouldReturnNotFlaggedMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -188,7 +188,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListReadFilterShouldWork() throws Exception {
+    public void getMessageListReadFilterShouldReturnOnlyReadMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotRead = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -212,7 +212,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListUnreadFilterShouldWork() throws Exception {
+    public void getMessageListUnreadFilterShouldReturnOnlyUnreadMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotRead = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -236,7 +236,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListSetDraftFilterShouldWork() throws Exception {
+    public void getMessageListSetDraftFilterShouldReturnOnlyDraftMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotDraft = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -260,7 +260,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListUnsetDraftFilterShouldWork() throws Exception {
+    public void getMessageListUnsetDraftFilterShouldReturnOnlyNonDraftMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotDraft = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -284,7 +284,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListSetAnsweredFilterShouldWork() throws Exception {
+    public void getMessageListSetAnsweredFilterShouldReturnOnlyAnsweredMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotAnswered = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -308,7 +308,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListUnsetAnsweredFilterShouldWork() throws Exception {
+    public void getMessageListUnsetAnsweredFilterShouldReturnOnlyNotAnsweredMessages() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotAnswered = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -332,7 +332,7 @@ public abstract class GetMessageListMethodTest {
     }
     
     @Test
-    public void getMessageListANDOperatorShouldWork() throws Exception {
+    public void getMessageListANDOperatorShouldReturnMessagesWhichMatchAllConditions() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotSeenNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -362,7 +362,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListOROperatorShouldWork() throws Exception {
+    public void getMessageListOROperatorShouldReturnMessagesWhichMatchOneOfAllConditions() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotSeenNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -392,7 +392,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListNOTOperatorShouldWork() throws Exception {
+    public void getMessageListNOTOperatorShouldReturnMessagesWhichNotMatchAnyCondition() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotSeenNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -422,7 +422,7 @@ public abstract class GetMessageListMethodTest {
     }
 
     @Test
-    public void getMessageListNestedOperatorsShouldWork() throws Exception {
+    public void getMessageListNestedOperatorsShouldReturnMessagesWhichMatchConditions() throws Exception {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         ComposedMessageId messageNotSeenNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
@@ -1332,6 +1332,256 @@ public abstract class GetMessageListMethodTest {
         ;
     }
 
+    @Test
+    public void getMessageListHasKeywordFilterShouldReturnMessagesWithKeywords() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.FLAGGED));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"hasKeyword\":\"$Flagged\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize()),
+                not(containsInAnyOrder(messageNotFlagged.getMessageId().serialize()))));
+    }
+
+    @Test
+    public void getMessageListHasKeywordFilterShouldReturnMessagesWithUserKeywords() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        Flags flags = FlagsBuilder.builder()
+            .add(Flags.Flag.FLAGGED)
+            .add(FORWARDED)
+            .build();
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, flags);
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"operator\":\"AND\",\"conditions\":[{\"hasKeyword\":\"$Flagged\"},{\"hasKeyword\":\"$Forwarded\"}]}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+            containsInAnyOrder(messageFlagged.getMessageId().serialize()),
+            not(containsInAnyOrder(messageNotFlagged.getMessageId().serialize()))));
+    }
+
+    @Test
+    public void getMessageListNotKeywordFilterShouldReturnMessagesWithoutKeywords() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.FLAGGED));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"notKeyword\":\"$Flagged\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageNotFlagged.getMessageId().serialize()),
+                not(containsInAnyOrder(messageFlagged.getMessageId().serialize()))));
+    }
+
+    @Test
+    public void getMessageListNotKeywordFilterShouldReturnMessagesWithoutUserKeywords() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        Flags flags = FlagsBuilder.builder()
+            .add(Flags.Flag.FLAGGED)
+            .add(FORWARDED)
+            .build();
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, flags);
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"operator\":\"AND\",\"conditions\":[{\"notKeyword\":\"$Flagged\"},{\"notKeyword\":\"$Forwarded\"}]}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageNotFlagged.getMessageId().serialize()),
+                not(containsInAnyOrder(messageFlagged.getMessageId().serialize()))));
+    }
+
+    @Test
+    public void getMessageListNotKeywordFilterShouldReturnMessagesWithoutKeywordsWhenMultipleNotKeywordAndFilterOperator() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        Flags flags = FlagsBuilder.builder()
+            .add(FORWARDED)
+            .add(Flags.Flag.DRAFT)
+            .build();
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.FLAGGED));
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, flags);
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"operator\":\"OR\",\"conditions\":[{\"notKeyword\":\"$Flagged\"},{\"notKeyword\":\"$Forwarded\"}]}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageNotFlagged.getMessageId().serialize(), messageFlagged.getMessageId().serialize())));
+    }
+
+    @Test
+    public void getMessageListHasKeywordAndNotKeywordFilterShouldReturnMessagesWithAndWithoutKeywords() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.FLAGGED));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"hasKeyword\":\"$Flagged\", \"notKeyword\":\"$Draft\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize()),
+                not(containsInAnyOrder(messageNotFlagged.getMessageId().serialize()))));
+    }
+
+    @Test
+    public void getMessageListHasKeywordShouldIgnoreDeleted() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.DELETED));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"hasKeyword\":\"$Deleted\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize(), messageNotFlagged.getMessageId().serialize())));
+    }
+
+    @Test
+    public void getMessageListHasKeywordShouldIgnoreRecent() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.RECENT));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"hasKeyword\":\"$Recent\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize(), messageNotFlagged.getMessageId().serialize())));
+    }
+
+    @Test
+    public void getMessageListNotKeywordShouldIgnoreDeleted() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.DELETED));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"notKeyword\":\"$Deleted\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize(), messageNotFlagged.getMessageId().serialize())));
+    }
+
+    @Test
+    public void getMessageListNotKeywordShouldIgnoreRecent() throws Exception {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
+
+        ComposedMessageId messageNotFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+        ComposedMessageId messageFlagged = mailboxProbe.appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.RECENT));
+
+        await();
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"notKeyword\":\"$Recent\"}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messageList"))
+            .body(ARGUMENTS + ".messageIds", allOf(
+                containsInAnyOrder(messageFlagged.getMessageId().serialize(), messageNotFlagged.getMessageId().serialize())));
+    }
+
     private Date convertToDate(LocalDate localDate) {
         return Date.from(localDate.atStartOfDay(ZONE_ID).toInstant());
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org