You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/09/15 02:27:31 UTC

[james-project] 03/06: JAMES-3368 Porting Keywords + Keyword to jmap-rfc-8621

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

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

commit 6d12c6569f1411cc43302bf45de4e48243ef760f
Author: duc91 <vd...@linagora.com>
AuthorDate: Fri Sep 4 16:53:30 2020 +0700

    JAMES-3368 Porting Keywords + Keyword to jmap-rfc-8621
---
 .../org/apache/james/jmap/model/Keyword.scala      |  84 +++++++++++++
 .../org/apache/james/jmap/model/Keywords.scala     | 138 +++++++++++++++++++++
 2 files changed, 222 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala
new file mode 100644
index 0000000..2cd37e4
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala
@@ -0,0 +1,84 @@
+/****************************************************************
+ * 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.base.{MoreObjects, Objects}
+import com.google.common.collect.{ImmutableBiMap, ImmutableList}
+import com.ibm.icu.text.UnicodeSet
+import javax.mail.Flags
+import org.apache.commons.lang3.StringUtils
+
+object Keyword {
+  private val FLAG_NAME_MIN_LENGTH = 1
+  private val FLAG_NAME_MAX_LENGTH = 255
+  private val FLAG_NAME_PATTERN = new UnicodeSet("[[a-z][A-Z][0-9]]").add('$').add('_').add('-').freeze
+  val DRAFT = Keyword.of("$Draft")
+  val SEEN = Keyword.of("$Seen")
+  val FLAGGED = Keyword.of("$Flagged")
+  val ANSWERED = Keyword.of("$Answered")
+  val DELETED = Keyword.of("$Deleted")
+  val RECENT = Keyword.of("$Recent")
+  val FORWARDED = Keyword.of("$Forwarded")
+  val FLAG_VALUE = true
+  private val NON_EXPOSED_IMAP_KEYWORDS = ImmutableList.of(Keyword.RECENT, Keyword.DELETED)
+  private val IMAP_SYSTEM_FLAGS = ImmutableBiMap.builder[Flags.Flag, Keyword]
+    .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 val VALIDATION_MESSAGE = """FlagName must not be null or empty, must have length form 1-255,
+      | must not contain characters with hex from '\\u0000' to '\\u00019'
+      | or {'(' ')' '{' ']' '%' '*' '\"' '\\'} """.stripMargin
+
+  def parse(flagName: String): Either[String, Keyword] = {
+    if (isValid(flagName)) Right(new Keyword(flagName))
+    Left(VALIDATION_MESSAGE)
+  }
+
+  def of(flagName: String): Keyword = parse(flagName) match {
+    case Left(errorMessage: String) => throw new IllegalArgumentException(errorMessage)
+    case Right(keyword: Keyword) => keyword
+  }
+
+  def fromFlag(flag: Flags.Flag): Option[Keyword] = Some(IMAP_SYSTEM_FLAGS.get(flag))
+
+  def isValid(flagName: String): Boolean = flagName match {
+      case _ if StringUtils.isBlank(flagName) => false
+      case _ if flagName.length < FLAG_NAME_MIN_LENGTH || flagName.length > FLAG_NAME_MAX_LENGTH => false
+      case _ => FLAG_NAME_PATTERN.containsAll(flagName)
+    }
+}
+
+class Keyword(val flagName: String) {
+  def getFlagName(): String = flagName
+
+  def isExposedImapKeyword(): Boolean = !Keyword.NON_EXPOSED_IMAP_KEYWORDS.contains(this)
+
+  def isDraft(): Boolean = Keyword.DRAFT == this
+
+  def asSystemFlag(): Optional[Flags.Flag] = Optional.ofNullable(Keyword.IMAP_SYSTEM_FLAGS.inverse.get(this))
+
+  def asFlags(): Flags = asSystemFlag().map(Flag => new Flags(Flag)).orElse(new Flags(flagName))
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keywords.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keywords.scala
new file mode 100644
index 0000000..7ef0540
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keywords.scala
@@ -0,0 +1,138 @@
+/****************************************************************
+ * 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 com.google.common.base.{MoreObjects, Objects}
+import javax.mail.Flags
+import org.apache.james.jmap.model.KeywordsFactory.{LENIENT_KEYWORDS_FACTORY, STRICT_KEYWORDS_FACTORY}
+import org.apache.james.mailbox.FlagsBuilder
+
+import scala.collection.IterableOnce
+import scala.jdk.CollectionConverters._
+
+trait ToKeyword {
+      def toKeyword(value: String): Option[Keyword]
+}
+
+object ToKeyword {
+  val STRICT: ToKeyword = (value: String) => Some(Keyword.of(value))
+  val LENIENT: ToKeyword = (value: String) => Keyword.parse(value) match {
+    case Left(errorMessage: String) => {
+      println(errorMessage)
+      None
+    }
+    case Right(value: Keyword) => Some(value)
+  }
+}
+
+trait KeywordsValidator {
+      def validate(keywords: Set[Keyword]): Boolean
+}
+
+object KeywordsValidator {
+      val THROW_ON_IMAP_NON_EXPOSED_KEYWORDS: KeywordsValidator = (keywords: Set[Keyword]) => {
+        if (keywords.exists(KeywordFilter.FILTER_IMAP_NON_EXPOSED_KEYWORDS)) {
+          throw new IllegalArgumentException("Does not allow to update 'Deleted' or 'Recent' flag")
+        }
+        true
+      }
+
+      val IGNORE_NON_EXPOSED_IMAP_KEYWORDS: KeywordsValidator = (keywords: Set[Keyword]) => {
+        def foo(keywords: Set[Keyword]): Option[Keyword] = ???
+        foo(keywords).isDefined
+      }
+    }
+
+object KeywordFilter {
+      val FILTER_IMAP_NON_EXPOSED_KEYWORDS: Keyword => Boolean = (keyword: Keyword) => keyword.isExposedImapKeyword()
+      val KEEP_ALL: Keyword => Boolean = (_: Keyword) => true
+}
+
+object KeywordsFactory {
+    val STRICT_KEYWORDS_FACTORY = new KeywordsFactory(KeywordsValidator.THROW_ON_IMAP_NON_EXPOSED_KEYWORDS, KeywordFilter.KEEP_ALL, ToKeyword.STRICT)
+    val LENIENT_KEYWORDS_FACTORY = new KeywordsFactory(KeywordsValidator.IGNORE_NON_EXPOSED_IMAP_KEYWORDS, KeywordFilter.FILTER_IMAP_NON_EXPOSED_KEYWORDS, ToKeyword.LENIENT)
+
+    private[model] trait KeywordFilter extends IterableOnce[Keyword] {}
+}
+
+object Keywords {
+  val DEFAULT_VALUE: Set[Keywords] = Set()
+}
+
+case class Keywords(keywords: Set[Keyword]) {
+
+  def combiner(firstBuilder: FlagsBuilder, secondBuilder: FlagsBuilder): FlagsBuilder = firstBuilder.add(secondBuilder.build)
+  def accumulator(accumulator: FlagsBuilder, keyword: Keyword): FlagsBuilder = accumulator.add(keyword.asFlags())
+
+  /**
+   * This Keywords factory will filter out invalid keywords (containing inavlid character, being too long or being non
+   * exposed IMAP flags)
+   */
+  def strictFactory: KeywordsFactory = STRICT_KEYWORDS_FACTORY
+
+  /**
+   * This Keywords factory will throw upon invalid keywords (containing inavlid character, being too long or being non
+   * exposed IMAP flags)
+   *
+   * @throws IllegalArgumentException
+   */
+  def lenientFactory: KeywordsFactory = LENIENT_KEYWORDS_FACTORY
+
+  def asFlags(): Flags = keywords.asJava.stream().reduce(FlagsBuilder.builder, this.accumulator, this.combiner).build
+
+  def asFlagsWithRecentAndDeletedFrom(originFlags: Flags): Flags = {
+    val flags = asFlags
+    if (originFlags.contains(Flags.Flag.DELETED)) flags.add(Flags.Flag.DELETED)
+    if (originFlags.contains(Flags.Flag.RECENT)) flags.add(Flags.Flag.RECENT)
+    flags
+  }
+
+  def asMap: Map[String, Boolean] = keywords zip Iterable(Keyword.FLAG_VALUE) toMap
+
+  def getKeywords: Set[Keyword] = keywords
+
+  def contains(keyword: Keyword): Boolean = keywords.contains(keyword)
+}
+
+class KeywordsFactory(val validator: KeywordsValidator, val filter: Keyword => Boolean, val toKeyword: ToKeyword) {
+    def fromSet(setKeywords: Set[Keyword]): Keywords = {
+      validator.validate(setKeywords)
+      Keywords(setKeywords.filter(filter))
+    }
+
+    def from(keywords: Keyword*): Keywords = {
+      fromSet(keywords.toSet)
+    }
+
+    def fromCollection(keywords: Set[String]): Keywords = fromSet(keywords.flatMap(toKeyword.toKeyword))
+
+    def fromMap(mapKeywords: Map[String, Boolean]): Keywords = {
+      if (mapKeywords.values.toSet.exists(value => value)) {
+          throw new IllegalArgumentException("Keyword must be true")
+      }
+      fromCollection(mapKeywords.keySet)
+    }
+
+    def fromFlags(flags: Flags): Keywords = {
+      val fromUserFlags: Set[Option[Keyword]] = flags.getUserFlags.map[Option[Keyword]](toKeyword.toKeyword).toSet
+      val fromSystemFlags: Set[Option[Keyword]] = flags.getSystemFlags.map[Option[Keyword]](Keyword.fromFlag).toSet
+
+      fromSet((fromUserFlags ++ fromSystemFlags).flatten(option => option.isDefined).map(_.get))
+    }
+  }
\ No newline at end of file


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