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