MAILBOX-270: Apache James supports metadata extension (RFC5464), store metadata


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

Branch: refs/heads/master
Commit: 60b612aed5c9996039c4c2b9731888d0c6d1f941
Parents: 6e153c4
Author: Quynh Nguyen <[email protected]>
Authored: Thu Jun 2 16:23:56 2016 +0700
Committer: Benoit Tellier <[email protected]>
Committed: Thu Jun 23 11:35:54 2016 +0700

----------------------------------------------------------------------
 mailbox/api/pom.xml                             |   9 ++
 .../apache/james/mailbox/MailboxManager.java    |  42 +++++-
 .../james/mailbox/model/MailboxAnnotation.java  | 105 +++++++++++++
 .../james/mailbox/MailboxManagerTest.java       | 102 +++++++++++++
 .../mailbox/model/MailboxAnnotationTest.java    | 128 ++++++++++++++++
 .../mailbox/caching/CachingMailboxMapper.java   |   1 -
 .../CachingMailboxSessionMapperFactory.java     |   9 ++
 .../cassandra/CassandraMailboxManager.java      |   2 +-
 .../CassandraMailboxSessionMapperFactory.java   |  10 ++
 .../mail/CassandraAnnotationMapper.java         |  98 ++++++++++++
 .../modules/CassandraAnnotationModule.java      |  68 +++++++++
 .../table/CassandraAnnotationTable.java         |  30 ++++
 .../cassandra/CassandraMailboxManagerTest.java  |   6 +-
 .../cassandra/mail/CassandraMapperProvider.java |  15 +-
 .../hbase/HBaseMailboxSessionMapperFactory.java |   9 ++
 .../jcr/JCRMailboxSessionMapperFactory.java     |   9 ++
 .../jpa/JPAMailboxSessionMapperFactory.java     |   9 ++
 .../MaildirMailboxSessionMapperFactory.java     |  10 ++
 .../inmemory/InMemoryMailboxManager.java        |   2 +-
 .../InMemoryMailboxSessionMapperFactory.java    |   9 ++
 .../inmemory/mail/InMemoryAnnotationMapper.java | 112 ++++++++++++++
 .../inmemory/mail/InMemoryMapperProvider.java   |   7 +
 mailbox/pom.xml                                 |   5 +
 .../store/MailboxSessionMapperFactory.java      |  15 +-
 .../mailbox/store/StoreMailboxManager.java      |  42 +++++-
 .../mailbox/store/mail/AnnotationMapper.java    |  62 ++++++++
 .../StoreMailboxManagerAnnotationTest.java      | 148 +++++++++++++++++++
 .../store/mail/model/AnnotationMapperTest.java  | 117 +++++++++++++++
 .../store/mail/model/MapperProvider.java        |   3 +
 mpt/impl/imap-mailbox/elasticsearch/.gitignore  |   1 +
 .../base/MailboxEventAnalyserTest.java          |  24 +++
 .../FirstUserConnectionFilterThreadTest.java    |  23 ++-
 32 files changed, 1221 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/api/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/api/pom.xml b/mailbox/api/pom.xml
index 12999c6..9dbca1d 100644
--- a/mailbox/api/pom.xml
+++ b/mailbox/api/pom.xml
@@ -37,6 +37,10 @@
             <artifactId>guava</artifactId>
         </dependency>
         <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+        </dependency>
+        <dependency>
             <groupId>${javax.mail.groupId}</groupId>
             <artifactId>${javax.mail.artifactId}</artifactId>
         </dependency>
@@ -61,6 +65,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-guava</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
----------------------------------------------------------------------
diff --git 
a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java 
b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
index 4e597b8..cfbdc15 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
@@ -21,6 +21,7 @@ package org.apache.james.mailbox;
 
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.james.mailbox.exception.BadCredentialsException;
 import org.apache.james.mailbox.exception.MailboxException;
@@ -28,6 +29,7 @@ import 
org.apache.james.mailbox.exception.MailboxExistsException;
 import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.exception.UnsupportedRightException;
 import org.apache.james.mailbox.model.MailboxACL;
+import org.apache.james.mailbox.model.MailboxAnnotation;
 import org.apache.james.mailbox.model.MailboxMetaData;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.model.MailboxQuery;
@@ -73,11 +75,14 @@ public interface MailboxManager extends RequestAware, 
MailboxListenerSupport {
     enum MailboxCapabilities {
         Move,
         UserFlag,
-        Namespace
+        Namespace,
+        Annotation
     }
 
     EnumSet<MailboxCapabilities> getSupportedMailboxCapabilities();
-    
+
+    boolean hasCapability(MailboxCapabilities capability);
+
     enum MessageCapabilities {
         Attachment
     }
@@ -336,4 +341,37 @@ public interface MailboxManager extends RequestAware, 
MailboxListenerSupport {
      */
     List<MailboxPath> list(MailboxSession session) throws MailboxException;
 
+    /**
+     * Return all mailbox's annotation as the {@link List} of {@link 
MailboxAnnotation} without order and not duplicated by key
+     * 
+     * @param mailboxPath   the current mailbox
+     * @param session       the current session
+     * @return              List<MailboxAnnotation>
+     * @throws MailboxException in case of selected mailbox does not exist
+     */
+    List<MailboxAnnotation> getAllAnnotations(MailboxPath mailboxPath, 
MailboxSession session) throws MailboxException;
+
+    /**
+     * Return all mailbox's annotation filter by the list of the keys without 
order and not duplicated by key
+     * 
+     * @param mailboxPath   the current mailbox
+     * @param session       the current session
+     * @param keys          list of the keys should be filter
+     * @return              List<MailboxAnnotation>
+     * @throws MailboxException in case of selected mailbox does not exist
+     */
+    List<MailboxAnnotation> getAnnotationsByKeys(MailboxPath mailboxPath, 
MailboxSession session, Set<String> keys) throws MailboxException;
+
+    /**
+     * Update the mailbox's annotations. This method can:
+     * - Insert new annotation if it does not exist
+     * - Update the new value for existed annotation
+     * - Delete the existed annotation if its value is nil
+     * 
+     * @param mailboxPath   the current mailbox
+     * @param session       the current session
+     * @param mailboxAnnotations    the list of annotation should be 
insert/udpate/delete
+     * @throws MailboxException in case of selected mailbox does not exist
+     */
+    void updateAnnotations(MailboxPath mailboxPath, MailboxSession session, 
List<MailboxAnnotation> mailboxAnnotations) throws MailboxException;
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxAnnotation.java
----------------------------------------------------------------------
diff --git 
a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxAnnotation.java
 
b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxAnnotation.java
new file mode 100644
index 0000000..b2152d4
--- /dev/null
+++ 
b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxAnnotation.java
@@ -0,0 +1,105 @@
+/****************************************************************
+ * 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.model;
+
+import org.apache.commons.lang.StringUtils;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+public class MailboxAnnotation {
+
+    private static final CharMatcher NAME_ANNOTATION_PATTERN = 
CharMatcher.ASCII
+            .and(CharMatcher.inRange('\u0000', 
'\u0019').negate()).and(CharMatcher.isNot('*'))
+            .and(CharMatcher.isNot('%'));
+    
+    public static final String SLASH_CHARACTER = "/";
+    
+    public static final String TWO_SLASH_CHARACTER = "//";
+    
+    public static MailboxAnnotation nil(String key) {
+        return new MailboxAnnotation(key, Optional.<String> absent());
+    }
+
+    public static MailboxAnnotation newInstance(String key, String value) {
+        return new MailboxAnnotation(key, Optional.of(value));
+    }
+
+    private final String key;
+    private final Optional<String> value;
+
+    private MailboxAnnotation(String key, Optional<String> value) {
+        Preconditions.checkNotNull(key);
+        Preconditions.checkNotNull(value);
+        Preconditions.checkArgument(isValidKey(key), 
+                "Key must start with '/' and not end with '/' and does not 
contain charater with hex from '\u0000' to '\u00019' or {'*', '%', two 
consecutive '/'} ");
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public Optional<String> getValue() {
+        return value;
+    }
+
+    private static boolean isValidKey(String input) {
+        if (StringUtils.isBlank(input)) {
+            return false;
+        }
+        String key = input.trim();
+        if (!key.startsWith(SLASH_CHARACTER)) {
+            return false;
+        }
+        if (key.contains(TWO_SLASH_CHARACTER)) {
+            return false;
+        }
+        if (key.endsWith(SLASH_CHARACTER)) {
+            return false;
+        }
+        if (!NAME_ANNOTATION_PATTERN.matchesAllOf(key)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(key, value);
+    }
+
+    public boolean isNil() {
+        return !value.isPresent();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof MailboxAnnotation) {
+            MailboxAnnotation o = (MailboxAnnotation) obj;
+            return Objects.equal(key, o.getKey()) && Objects.equal(value, 
o.getValue());
+        } else {
+            return false;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
----------------------------------------------------------------------
diff --git 
a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java 
b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
index 6c9e741..8504123 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
@@ -27,9 +27,11 @@ import java.util.List;
 
 import javax.mail.Flags;
 
+import org.apache.james.mailbox.MailboxManager.MailboxCapabilities;
 import org.apache.james.mailbox.exception.BadCredentialsException;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.mock.MockMailboxManager;
+import org.apache.james.mailbox.model.MailboxAnnotation;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxMetaData;
 import org.apache.james.mailbox.model.MailboxPath;
@@ -43,6 +45,9 @@ import org.xenei.junit.contract.Contract;
 import org.xenei.junit.contract.ContractTest;
 import org.xenei.junit.contract.IProducer;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
 /**
  * Test the {@link StoreMailboxManager} methods that 
  * are not covered by the protocol-tester suite.
@@ -58,6 +63,12 @@ public class MailboxManagerTest<T extends MailboxManager> {
     public final static String USER_1 = "USER_1";
     public final static String USER_2 = "USER_2";
 
+    private static final MailboxAnnotation PRIVATE_ANNOTATION = 
MailboxAnnotation.newInstance("/private/comment", "My private comment");
+    private static final MailboxAnnotation PRIVATE_ANNOTATION_UPDATE = 
MailboxAnnotation.newInstance("/private/comment", "My updated private comment");
+    private static final MailboxAnnotation SHARED_ANNOTATION =  
MailboxAnnotation.newInstance("/shared/comment", "My shared comment");
+
+    private static final List<MailboxAnnotation> ANNOTATIONS = 
ImmutableList.of(PRIVATE_ANNOTATION, SHARED_ANNOTATION);
+
     @Rule
     public ExpectedException expected = ExpectedException.none();
 
@@ -231,4 +242,95 @@ public class MailboxManagerTest<T extends MailboxManager> {
         
assertThat(metaDatas.get(0).getPath()).isEqualTo(MailboxPath.inbox(session));
     }
 
+    @ContractTest
+    public void updateAnnotationsShouldUpdateStoredAnnotation() throws 
MailboxException {
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+        mailboxManager.createMailbox(inbox, session);
+
+        mailboxManager.updateAnnotations(inbox, session, 
ImmutableList.of(PRIVATE_ANNOTATION));
+
+        mailboxManager.updateAnnotations(inbox, session, 
ImmutableList.of(PRIVATE_ANNOTATION_UPDATE));
+        assertThat(mailboxManager.getAllAnnotations(inbox, 
session)).containsOnly(PRIVATE_ANNOTATION_UPDATE);
+    }
+
+    @ContractTest
+    public void updateAnnotationsShouldDeleteAnnotationWithNilValue() throws 
BadCredentialsException, MailboxException {
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+        mailboxManager.createMailbox(inbox, session);
+
+        mailboxManager.updateAnnotations(inbox, session, 
ImmutableList.of(PRIVATE_ANNOTATION));
+
+        mailboxManager.updateAnnotations(inbox, session, 
ImmutableList.of(MailboxAnnotation.nil("/private/comment")));
+        assertThat(mailboxManager.getAllAnnotations(inbox, session)).isEmpty();
+    }
+
+    @ContractTest
+    public void updateAnnotationsShouldThrowExceptionIfMailboxDoesNotExist() 
throws MailboxException {
+        expected.expect(MailboxException.class);
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+
+        mailboxManager.updateAnnotations(inbox, session, 
ImmutableList.of(PRIVATE_ANNOTATION));
+    }
+
+    @ContractTest
+    public void getAnnotationsShouldReturnEmptyForNonStoredAnnotation() throws 
BadCredentialsException, MailboxException {
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+        mailboxManager.createMailbox(inbox, session);
+
+        assertThat(mailboxManager.getAllAnnotations(inbox, session)).isEmpty();
+    }
+
+    @ContractTest
+    public void getAllAnnotationsShouldRetrieveStoredAnnotations() throws 
BadCredentialsException, MailboxException {
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+        mailboxManager.createMailbox(inbox, session);
+
+        mailboxManager.updateAnnotations(inbox, session, ANNOTATIONS);
+
+        assertThat(mailboxManager.getAllAnnotations(inbox, 
session)).isEqualTo(ANNOTATIONS);
+    }
+
+    @ContractTest
+    public void getAllAnnotationsShouldThrowExceptionIfMailboxDoesNotExist() 
throws MailboxException {
+        expected.expect(MailboxException.class);
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+
+        mailboxManager.getAllAnnotations(inbox, session);
+    }
+
+    @ContractTest
+    public void getAnnotationsByKeysShouldRetrieveStoresAnnotationsByKeys() 
throws BadCredentialsException, MailboxException {
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+        mailboxManager.createMailbox(inbox, session);
+
+        mailboxManager.updateAnnotations(inbox, session, ANNOTATIONS);
+
+        assertThat(mailboxManager.getAnnotationsByKeys(inbox, session, 
ImmutableSet.of("/private/comment")))
+            .containsOnly(PRIVATE_ANNOTATION);
+    }
+
+    @ContractTest
+    public void 
getAnnotationsByKeysShouldThrowExceptionIfMailboxDoesNotExist() throws 
MailboxException {
+        expected.expect(MailboxException.class);
+        
Assume.assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.Annotation));
+        session = mailboxManager.createSystemSession(USER_2, 
LoggerFactory.getLogger("Test"));
+        MailboxPath inbox = MailboxPath.inbox(session);
+
+        mailboxManager.getAnnotationsByKeys(inbox, session, 
ImmutableSet.of("/private/comment"));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxAnnotationTest.java
----------------------------------------------------------------------
diff --git 
a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxAnnotationTest.java
 
b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxAnnotationTest.java
new file mode 100644
index 0000000..ba85a58
--- /dev/null
+++ 
b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxAnnotationTest.java
@@ -0,0 +1,128 @@
+/****************************************************************
+ * 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.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+public class MailboxAnnotationTest {
+    private static final String ASTERISK_CHARACTER = "*";
+
+    private static final String PERCENT_CHARACTER = "%";
+
+    private static final String ANY_KEY = "shared";
+    private static final String ANNOTATION_KEY = "/private/comment"; 
+    private static final String ANNOTATION_VALUE = "anyValue";
+
+    @Test(expected = NullPointerException.class)
+    public void newInstanceShouldThrowsExceptionWithNullKey() throws Exception 
{
+        MailboxAnnotation.newInstance(null, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void newInstanceShouldThrowsExceptionWithNullValue() throws 
Exception {
+        MailboxAnnotation.newInstance(ANY_KEY, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void newInstanceShouldThrowsExceptionWithEmptyKey() throws 
Exception {
+        MailboxAnnotation.newInstance("", ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void newInstanceShouldThrowsExceptionWithBlankKey() throws 
Exception {
+        MailboxAnnotation.newInstance("   ", ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void newInstanceShouldThrowsExceptionWhenKeyDoesNotStartWithSlash() 
throws Exception {
+        MailboxAnnotation.newInstance(ANY_KEY, ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void newInstanceShouldThrowsExceptionWhenKeyContainsAsterisk() 
throws Exception {
+        MailboxAnnotation.newInstance(buildAnnotationKey(ASTERISK_CHARACTER), 
ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void newInstanceShouldThrowsExceptionWhenKeyContainsPercent() 
throws Exception {
+        MailboxAnnotation.newInstance(buildAnnotationKey(PERCENT_CHARACTER), 
ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void 
validKeyShouldThrowsExceptionWhenKeyContainsTwoConsecutiveSlash() throws 
Exception {
+        
MailboxAnnotation.newInstance(buildAnnotationKey(MailboxAnnotation.TWO_SLASH_CHARACTER),
 ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void validKeyShouldThrowsExceptionWhenKeyEndsWithSlash() throws 
Exception {
+        
MailboxAnnotation.newInstance(buildAnnotationKey(MailboxAnnotation.SLASH_CHARACTER)
 + MailboxAnnotation.SLASH_CHARACTER, 
+            ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void validKeyShouldThrowsExceptionWhenKeyContainsNonASCII() throws 
Exception {
+        MailboxAnnotation.newInstance(buildAnnotationKey(" "), 
ANNOTATION_VALUE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void validKeyShouldThrowsExceptionWhenKeyContainsTabCharacter() 
throws Exception {
+        MailboxAnnotation.newInstance(buildAnnotationKey("\t"), 
ANNOTATION_VALUE);
+    }
+
+    @Test
+    public void nilInstanceShouldReturnAbsentValue() throws Exception {
+        MailboxAnnotation annotation = MailboxAnnotation.nil(ANNOTATION_KEY);
+
+        assertThat(annotation.getValue()).isAbsent();
+    }
+
+    @Test
+    public void isNilShouldReturnTrueForNilObject() throws Exception {
+        MailboxAnnotation nilAnnotation = 
MailboxAnnotation.nil(ANNOTATION_KEY);
+        assertThat(nilAnnotation.isNil()).isTrue();
+    }
+
+    @Test
+    public void isNilShouldReturnFalseForNotNilObject() throws Exception {
+        MailboxAnnotation nilAnnotation = 
MailboxAnnotation.newInstance(ANNOTATION_KEY, ANY_KEY);
+        assertThat(nilAnnotation.isNil()).isFalse();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void 
newInstanceMailboxAnnotationShouldThrowExceptionWithNullValue() throws 
Exception {
+        MailboxAnnotation.newInstance(ANY_KEY, null);
+    }
+
+    @Test
+    public void newInstanceMailboxAnnotationShouldCreateNewInstance() throws 
Exception {
+        MailboxAnnotation annotation = 
MailboxAnnotation.newInstance(ANNOTATION_KEY, ANNOTATION_VALUE);
+
+        assertThat(annotation.getKey()).contains(ANNOTATION_KEY);
+        assertThat(annotation.getValue()).contains(ANNOTATION_VALUE);
+    }
+
+    private String buildAnnotationKey(String apartOfKey) {
+        return MailboxAnnotation.SLASH_CHARACTER + ANY_KEY + apartOfKey + 
ANY_KEY;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxMapper.java
----------------------------------------------------------------------
diff --git 
a/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxMapper.java
 
b/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxMapper.java
index 8c5ee7f..3c034a9 100644
--- 
a/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxMapper.java
+++ 
b/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxMapper.java
@@ -86,5 +86,4 @@ public class CachingMailboxMapper implements MailboxMapper {
                cache.invalidate(mailbox);
        }
 
-
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxSessionMapperFactory.java
 
b/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxSessionMapperFactory.java
index 0495d59..2b9ee48 100644
--- 
a/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxSessionMapperFactory.java
+++ 
b/mailbox/caching/src/main/java/org/apache/james/mailbox/caching/CachingMailboxSessionMapperFactory.java
@@ -1,13 +1,16 @@
 package org.apache.james.mailbox.caching;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.NoopAttachmentMapper;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 /**
@@ -52,4 +55,10 @@ public class CachingMailboxSessionMapperFactory extends
         return new NoopAttachmentMapper();
     }
 
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        throw new NotImplementedException();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java
index 1e087bd..2219604 100644
--- 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java
@@ -64,7 +64,7 @@ public class CassandraMailboxManager extends 
StoreMailboxManager {
 
     @Override
     public EnumSet<MailboxManager.MailboxCapabilities> 
getSupportedMailboxCapabilities() {
-        return EnumSet.of(MailboxCapabilities.Move, 
MailboxCapabilities.UserFlag, MailboxCapabilities.Namespace);
+        return EnumSet.of(MailboxCapabilities.Move, 
MailboxCapabilities.UserFlag, MailboxCapabilities.Namespace, 
MailboxCapabilities.Annotation);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
index 7e18c26..77cf9d7 100644
--- 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
@@ -23,15 +23,19 @@ import javax.inject.Inject;
 
 import org.apache.james.backends.cassandra.init.CassandraTypesProvider;
 import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.cassandra.mail.CassandraAnnotationMapper;
 import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentMapper;
 import org.apache.james.mailbox.cassandra.mail.CassandraMailboxMapper;
 import org.apache.james.mailbox.cassandra.mail.CassandraMessageMapper;
 import org.apache.james.mailbox.cassandra.user.CassandraSubscriptionMapper;
+import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.ModSeqProvider;
 import org.apache.james.mailbox.store.mail.UidProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 import com.datastax.driver.core.Session;
@@ -93,4 +97,10 @@ public class CassandraMailboxSessionMapperFactory extends 
MailboxSessionMapperFa
     Session getSession() {
         return session;
     }
+
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession mailboxSession)
+            throws MailboxException {
+        return new CassandraAnnotationMapper((CassandraId)mailboxId, session);
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraAnnotationMapper.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraAnnotationMapper.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraAnnotationMapper.java
new file mode 100644
index 0000000..a96ea69
--- /dev/null
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraAnnotationMapper.java
@@ -0,0 +1,98 @@
+/****************************************************************
+ * 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.cassandra.mail;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.delete;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.james.backends.cassandra.utils.CassandraUtils;
+import org.apache.james.mailbox.cassandra.CassandraId;
+import org.apache.james.mailbox.cassandra.table.CassandraAnnotationTable;
+import org.apache.james.mailbox.model.MailboxAnnotation;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
+import org.apache.james.mailbox.store.transaction.NonTransactionalMapper;
+
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+public class CassandraAnnotationMapper extends NonTransactionalMapper 
implements AnnotationMapper {
+
+    private final CassandraId mailboxId;
+    private final Session session;
+
+    public CassandraAnnotationMapper(CassandraId mailboxId, Session session) {
+        this.mailboxId = mailboxId;
+        this.session = session;
+    }
+
+    public List<MailboxAnnotation> getAllAnnotations() {
+        return 
CassandraUtils.convertToStream(session.execute(getStoredAnnotationsQuery()))
+            .map(this::toAnnotation)
+            .collect(Collectors.toList());
+    }
+
+    public List<MailboxAnnotation> getAnnotationsByKeys(Set<String> keys) {
+        return 
CassandraUtils.convertToStream(session.execute(getStoredAnnotationsQueryForKeys(keys)))
+            .map(this::toAnnotation)
+            .collect(Collectors.toList());
+    }
+
+    public void deleteAnnotation(String key) {
+        session.execute(delete().from(CassandraAnnotationTable.TABLE_NAME)
+            .where(eq(CassandraAnnotationTable.MAILBOX_ID, mailboxId.asUuid()))
+            .and(eq(CassandraAnnotationTable.KEY, key)));
+    }
+
+    public void insertAnnotation(MailboxAnnotation mailboxAnnotation) {
+        Preconditions.checkArgument(!mailboxAnnotation.isNil());
+        session.execute(insertInto(CassandraAnnotationTable.TABLE_NAME)
+            .value(CassandraAnnotationTable.MAILBOX_ID, mailboxId.asUuid())
+            .value(CassandraAnnotationTable.KEY, mailboxAnnotation.getKey())
+            .value(CassandraAnnotationTable.VALUE, 
mailboxAnnotation.getValue().get()));
+    }
+
+    private MailboxAnnotation toAnnotation(Row row) {
+        return 
MailboxAnnotation.newInstance(row.getString(CassandraAnnotationTable.KEY), 
row.getString(CassandraAnnotationTable.VALUE));
+    }
+
+    private Select.Where getStoredAnnotationsQuery() {
+        return select(CassandraAnnotationTable.SELECT_FIELDS)
+                .from(CassandraAnnotationTable.TABLE_NAME)
+                .where(eq(CassandraAnnotationTable.MAILBOX_ID, 
mailboxId.asUuid()));
+    }
+
+    private Select.Where getStoredAnnotationsQueryForKeys(Set<String> keys) {
+        return 
getStoredAnnotationsQuery().and(in(CassandraAnnotationTable.KEY, 
Lists.newArrayList(keys)));
+    }
+
+    @Override
+    public void endRequest() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraAnnotationModule.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraAnnotationModule.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraAnnotationModule.java
new file mode 100644
index 0000000..443a065
--- /dev/null
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraAnnotationModule.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.cassandra.modules;
+
+import static com.datastax.driver.core.DataType.text;
+import static com.datastax.driver.core.DataType.timeuuid;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.james.backends.cassandra.components.CassandraIndex;
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.backends.cassandra.components.CassandraTable;
+import org.apache.james.backends.cassandra.components.CassandraType;
+import org.apache.james.mailbox.cassandra.table.CassandraAnnotationTable;
+
+import com.datastax.driver.core.schemabuilder.SchemaBuilder;
+
+public class CassandraAnnotationModule implements CassandraModule {
+    private final List<CassandraTable> tables;
+    private final List<CassandraIndex> index;
+    private final List<CassandraType> types;
+
+    public CassandraAnnotationModule() {
+        tables = Collections.singletonList(
+            new CassandraTable(CassandraAnnotationTable.TABLE_NAME,
+                SchemaBuilder.createTable(CassandraAnnotationTable.TABLE_NAME)
+                    .ifNotExists()
+                    .addPartitionKey(CassandraAnnotationTable.MAILBOX_ID, 
timeuuid())
+                    .addClusteringColumn(CassandraAnnotationTable.KEY, text())
+                    .addColumn(CassandraAnnotationTable.VALUE, text())));
+        index = Collections.emptyList();
+        types = Collections.emptyList();
+    }
+
+    @Override
+    public List<CassandraTable> moduleTables() {
+        return tables;
+    }
+
+    @Override
+    public List<CassandraIndex> moduleIndex() {
+        return index;
+    }
+
+    @Override
+    public List<CassandraType> moduleTypes() {
+        return types;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraAnnotationTable.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraAnnotationTable.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraAnnotationTable.java
new file mode 100644
index 0000000..fd9631b
--- /dev/null
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraAnnotationTable.java
@@ -0,0 +1,30 @@
+/****************************************************************
+ * 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.cassandra.table;
+
+public interface CassandraAnnotationTable {
+    String TABLE_NAME = "annotation";
+
+    String MAILBOX_ID = "mailboxId";
+    String KEY = "key";
+    String VALUE = "value";
+
+    String[] SELECT_FIELDS = { KEY, VALUE };
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
index 88387b1..230992d 100644
--- 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
+++ 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
@@ -23,13 +23,14 @@ import 
org.apache.james.backends.cassandra.init.CassandraModuleComposite;
 import org.apache.james.mailbox.cassandra.mail.CassandraModSeqProvider;
 import org.apache.james.mailbox.cassandra.mail.CassandraUidProvider;
 import org.apache.james.mailbox.cassandra.modules.CassandraAclModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraAnnotationModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraAttachmentModule;
 import 
org.apache.james.mailbox.cassandra.modules.CassandraMailboxCounterModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraMailboxModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraMessageModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraModSeqModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraSubscriptionModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraUidModule;
-import org.apache.james.mailbox.cassandra.modules.CassandraModSeqModule;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.store.NoMailboxPathLocker;
 import org.apache.james.mailbox.store.mail.model.impl.MessageParser;
@@ -53,7 +54,8 @@ public class CassandraMailboxManagerTest {
         new CassandraUidModule(),
         new CassandraModSeqModule(),
         new CassandraSubscriptionModule(),
-        new CassandraAttachmentModule()));
+        new CassandraAttachmentModule(),
+        new CassandraAnnotationModule()));
 
     private IProducer<CassandraMailboxManager> producer = new 
IProducer<CassandraMailboxManager>() {
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMapperProvider.java
----------------------------------------------------------------------
diff --git 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMapperProvider.java
 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMapperProvider.java
index 0bb915b..af504f6 100644
--- 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMapperProvider.java
+++ 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMapperProvider.java
@@ -23,6 +23,7 @@ import 
org.apache.james.backends.cassandra.init.CassandraModuleComposite;
 import org.apache.james.mailbox.cassandra.CassandraId;
 import org.apache.james.mailbox.cassandra.CassandraMailboxSessionMapperFactory;
 import org.apache.james.mailbox.cassandra.modules.CassandraAclModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraAnnotationModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraAttachmentModule;
 import 
org.apache.james.mailbox.cassandra.modules.CassandraMailboxCounterModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraMailboxModule;
@@ -31,6 +32,7 @@ import 
org.apache.james.mailbox.cassandra.modules.CassandraModSeqModule;
 import org.apache.james.mailbox.cassandra.modules.CassandraUidModule;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.mock.MockMailboxSession;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
@@ -45,7 +47,8 @@ public class CassandraMapperProvider implements 
MapperProvider {
         new CassandraMailboxCounterModule(),
         new CassandraModSeqModule(),
         new CassandraUidModule(),
-        new CassandraAttachmentModule()));
+        new CassandraAttachmentModule(),
+        new CassandraAnnotationModule()));
 
     @Override
     public MailboxMapper createMailboxMapper() throws MailboxException {
@@ -96,4 +99,14 @@ public class CassandraMapperProvider implements 
MapperProvider {
     public boolean supportPartialAttachmentFetch() {
         return true;
     }
+
+    @Override
+    public AnnotationMapper createAnnotationMapper() throws MailboxException {
+        return new CassandraMailboxSessionMapperFactory(
+                new CassandraUidProvider(cassandra.getConf()),
+                new CassandraModSeqProvider(cassandra.getConf()),
+                cassandra.getConf(),
+                cassandra.getTypesProvider()
+            ).getAnnotationMapper(CassandraId.timeBased(), new 
MockMailboxSession("benwa"));
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/hbase/src/main/java/org/apache/james/mailbox/hbase/HBaseMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/hbase/src/main/java/org/apache/james/mailbox/hbase/HBaseMailboxSessionMapperFactory.java
 
b/mailbox/hbase/src/main/java/org/apache/james/mailbox/hbase/HBaseMailboxSessionMapperFactory.java
index 0f93752..b2972a2 100644
--- 
a/mailbox/hbase/src/main/java/org/apache/james/mailbox/hbase/HBaseMailboxSessionMapperFactory.java
+++ 
b/mailbox/hbase/src/main/java/org/apache/james/mailbox/hbase/HBaseMailboxSessionMapperFactory.java
@@ -29,6 +29,7 @@ import static 
org.apache.james.mailbox.hbase.HBaseNames.SUBSCRIPTION_CF;
 
 import java.io.IOException;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.HColumnDescriptor;
 import org.apache.hadoop.hbase.HTableDescriptor;
@@ -43,12 +44,14 @@ import 
org.apache.james.mailbox.hbase.mail.HBaseMailboxMapper;
 import org.apache.james.mailbox.hbase.mail.HBaseMessageMapper;
 import org.apache.james.mailbox.hbase.user.HBaseSubscriptionMapper;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.ModSeqProvider;
 import org.apache.james.mailbox.store.mail.NoopAttachmentMapper;
 import org.apache.james.mailbox.store.mail.UidProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 /**
@@ -170,4 +173,10 @@ public class HBaseMailboxSessionMapperFactory extends 
MailboxSessionMapperFactor
     public UidProvider getUidProvider() {
         return uidProvider;
     }
+
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        throw new NotImplementedException();
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/jcr/src/main/java/org/apache/james/mailbox/jcr/JCRMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/jcr/src/main/java/org/apache/james/mailbox/jcr/JCRMailboxSessionMapperFactory.java
 
b/mailbox/jcr/src/main/java/org/apache/james/mailbox/jcr/JCRMailboxSessionMapperFactory.java
index 151fbb3..6fb5e43 100644
--- 
a/mailbox/jcr/src/main/java/org/apache/james/mailbox/jcr/JCRMailboxSessionMapperFactory.java
+++ 
b/mailbox/jcr/src/main/java/org/apache/james/mailbox/jcr/JCRMailboxSessionMapperFactory.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.mailbox.jcr;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
@@ -25,12 +26,14 @@ import org.apache.james.mailbox.jcr.mail.JCRMailboxMapper;
 import org.apache.james.mailbox.jcr.mail.JCRMessageMapper;
 import org.apache.james.mailbox.jcr.user.JCRSubscriptionMapper;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.ModSeqProvider;
 import org.apache.james.mailbox.store.mail.NoopAttachmentMapper;
 import org.apache.james.mailbox.store.mail.UidProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 /**
@@ -83,4 +86,10 @@ public class JCRMailboxSessionMapperFactory extends 
MailboxSessionMapperFactory
         return repository;
     }
 
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        throw new NotImplementedException();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java
 
b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java
index 7a3e4a1..9485ef5 100644
--- 
a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java
+++ 
b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java
@@ -21,18 +21,21 @@ package org.apache.james.mailbox.jpa;
 import javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.jpa.mail.JPAMailboxMapper;
 import org.apache.james.mailbox.jpa.mail.JPAMessageMapper;
 import org.apache.james.mailbox.jpa.user.JPASubscriptionMapper;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.ModSeqProvider;
 import org.apache.james.mailbox.store.mail.NoopAttachmentMapper;
 import org.apache.james.mailbox.store.mail.UidProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 /**
@@ -81,4 +84,10 @@ public class JPAMailboxSessionMapperFactory extends 
MailboxSessionMapperFactory
         return entityManagerFactory.createEntityManager();
     }
 
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        throw new NotImplementedException();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/maildir/src/main/java/org/apache/james/mailbox/maildir/MaildirMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/maildir/src/main/java/org/apache/james/mailbox/maildir/MaildirMailboxSessionMapperFactory.java
 
b/mailbox/maildir/src/main/java/org/apache/james/mailbox/maildir/MaildirMailboxSessionMapperFactory.java
index bd4c3f4..b129071 100644
--- 
a/mailbox/maildir/src/main/java/org/apache/james/mailbox/maildir/MaildirMailboxSessionMapperFactory.java
+++ 
b/mailbox/maildir/src/main/java/org/apache/james/mailbox/maildir/MaildirMailboxSessionMapperFactory.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.mailbox.maildir;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
@@ -25,10 +26,12 @@ import 
org.apache.james.mailbox.maildir.mail.MaildirMailboxMapper;
 import org.apache.james.mailbox.maildir.mail.MaildirMessageMapper;
 import org.apache.james.mailbox.maildir.user.MaildirSubscriptionMapper;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.NoopAttachmentMapper;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 public class MaildirMailboxSessionMapperFactory extends
@@ -66,4 +69,11 @@ public class MaildirMailboxSessionMapperFactory extends
         return new NoopAttachmentMapper();
     }
 
+
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        throw new NotImplementedException();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxManager.java
----------------------------------------------------------------------
diff --git 
a/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxManager.java
 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxManager.java
index 0986aff..78bac42 100644
--- 
a/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxManager.java
+++ 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxManager.java
@@ -44,7 +44,7 @@ public class InMemoryMailboxManager extends 
StoreMailboxManager {
 
     @Override
     public EnumSet<MailboxCapabilities> getSupportedMailboxCapabilities() {
-        return EnumSet.of(MailboxCapabilities.Move, 
MailboxCapabilities.UserFlag, MailboxCapabilities.Namespace);
+        return EnumSet.of(MailboxCapabilities.Move, 
MailboxCapabilities.UserFlag, MailboxCapabilities.Namespace, 
MailboxCapabilities.Annotation);
     }
     
     @Override

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxSessionMapperFactory.java
 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxSessionMapperFactory.java
index 25f0adc..fe70b1c 100644
--- 
a/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxSessionMapperFactory.java
+++ 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/InMemoryMailboxSessionMapperFactory.java
@@ -21,6 +21,7 @@ package org.apache.james.mailbox.inmemory;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.inmemory.mail.InMemoryAnnotationMapper;
 import org.apache.james.mailbox.inmemory.mail.InMemoryAttachmentMapper;
 import org.apache.james.mailbox.inmemory.mail.InMemoryMailboxMapper;
 import org.apache.james.mailbox.inmemory.mail.InMemoryMessageMapper;
@@ -28,9 +29,11 @@ import 
org.apache.james.mailbox.inmemory.mail.InMemoryModSeqProvider;
 import org.apache.james.mailbox.inmemory.mail.InMemoryUidProvider;
 import org.apache.james.mailbox.inmemory.user.InMemorySubscriptionMapper;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 
 public class InMemoryMailboxSessionMapperFactory extends 
MailboxSessionMapperFactory {
@@ -73,4 +76,10 @@ public class InMemoryMailboxSessionMapperFactory extends 
MailboxSessionMapperFac
         ((InMemorySubscriptionMapper) subscriptionMapper).deleteAll();
     }
 
+    @Override
+    public AnnotationMapper createAnnotationMapper(MailboxId mailboxId, 
MailboxSession session)
+            throws MailboxException {
+        return new InMemoryAnnotationMapper((InMemoryId)mailboxId);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/mail/InMemoryAnnotationMapper.java
----------------------------------------------------------------------
diff --git 
a/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/mail/InMemoryAnnotationMapper.java
 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/mail/InMemoryAnnotationMapper.java
new file mode 100644
index 0000000..66def77
--- /dev/null
+++ 
b/mailbox/memory/src/main/java/org/apache/james/mailbox/inmemory/mail/InMemoryAnnotationMapper.java
@@ -0,0 +1,112 @@
+/****************************************************************
+ * 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.inmemory.mail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.model.MailboxAnnotation;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Table;
+
+public class InMemoryAnnotationMapper implements AnnotationMapper {
+    private final InMemoryId mailboxId;
+    private final Table<InMemoryId, String, String> mailboxesAnnotations;
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public InMemoryAnnotationMapper(InMemoryId mailboxId) {
+        this.mailboxId = mailboxId;
+        mailboxesAnnotations = HashBasedTable.create();
+    }
+
+    @Override
+    public void endRequest() {
+
+    }
+
+    @Override
+    public <T> T execute(Transaction<T> transaction) throws MailboxException {
+        return transaction.run();
+    }
+
+    private Iterable<MailboxAnnotation> retrieveAllAnnotations(InMemoryId 
maiboxId) {
+        lock.readLock().lock();
+        try {
+            return Iterables.transform(
+                mailboxesAnnotations.row(maiboxId).entrySet(), 
+                new Function<Map.Entry<String, String>, MailboxAnnotation>() {
+                    public MailboxAnnotation apply(Entry<String, String> 
input) {
+                        return MailboxAnnotation.newInstance(input.getKey(), 
input.getValue());
+                    }
+                });
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+    
+    @Override
+    public List<MailboxAnnotation> getAllAnnotations() {
+        return ImmutableList.copyOf(retrieveAllAnnotations(mailboxId));
+    }
+
+    @Override
+    public List<MailboxAnnotation> getAnnotationsByKeys(final Set<String> 
keys) {
+        return ImmutableList.copyOf(
+            Iterables.filter(retrieveAllAnnotations(mailboxId),
+                new Predicate<MailboxAnnotation>() {
+                    public boolean apply(MailboxAnnotation input) {
+                        return keys.contains(input.getKey());
+                    }
+            }));
+    }
+
+    @Override
+    public void insertAnnotation(MailboxAnnotation mailboxAnnotation) {
+        lock.writeLock().lock();
+        try {
+            mailboxesAnnotations.put(mailboxId, mailboxAnnotation.getKey(), 
mailboxAnnotation.getValue().get());
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    @Override
+    public void deleteAnnotation(String key) {
+        lock.writeLock().lock();
+        try {
+            mailboxesAnnotations.remove(mailboxId, key);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/memory/src/test/java/org/apache/james/mailbox/inmemory/mail/InMemoryMapperProvider.java
----------------------------------------------------------------------
diff --git 
a/mailbox/memory/src/test/java/org/apache/james/mailbox/inmemory/mail/InMemoryMapperProvider.java
 
b/mailbox/memory/src/test/java/org/apache/james/mailbox/inmemory/mail/InMemoryMapperProvider.java
index 39a3623..b489e6f 100644
--- 
a/mailbox/memory/src/test/java/org/apache/james/mailbox/inmemory/mail/InMemoryMapperProvider.java
+++ 
b/mailbox/memory/src/test/java/org/apache/james/mailbox/inmemory/mail/InMemoryMapperProvider.java
@@ -6,6 +6,7 @@ import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.inmemory.InMemoryMailboxSessionMapperFactory;
 import org.apache.james.mailbox.mock.MockMailboxSession;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
@@ -53,4 +54,10 @@ public class InMemoryMapperProvider implements 
MapperProvider {
     public boolean supportPartialAttachmentFetch() {
         return false;
     }
+
+    @Override
+    public AnnotationMapper createAnnotationMapper() throws MailboxException {
+        return new 
InMemoryMailboxSessionMapperFactory().createAnnotationMapper(InMemoryId.of(random.nextInt()),
 
+            new MockMailboxSession("user"));
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/pom.xml b/mailbox/pom.xml
index a5e5d64..a7e1974 100644
--- a/mailbox/pom.xml
+++ b/mailbox/pom.xml
@@ -592,6 +592,11 @@
             <!--
                 END HBASE/HADOOP
             -->
+            <dependency>
+                <groupId>org.assertj</groupId>
+                <artifactId>assertj-guava</artifactId>
+                <version>1.3.1</version>
+            </dependency>
        </dependencies>
     </dependencyManagement>
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/store/src/main/java/org/apache/james/mailbox/store/MailboxSessionMapperFactory.java
----------------------------------------------------------------------
diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/MailboxSessionMapperFactory.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/MailboxSessionMapperFactory.java
index c5cc747..383c3a3 100644
--- 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/MailboxSessionMapperFactory.java
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/MailboxSessionMapperFactory.java
@@ -22,12 +22,14 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.RequestAware;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.AttachmentMapperFactory;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapperFactory;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.MessageMapperFactory;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.transaction.Mapper;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 import org.apache.james.mailbox.store.user.SubscriptionMapperFactory;
@@ -42,7 +44,7 @@ public abstract class MailboxSessionMapperFactory implements 
RequestAware, Mailb
     protected final static String MESSAGEMAPPER ="MESSAGEMAPPER";
     protected final static String MAILBOXMAPPER ="MAILBOXMAPPER";
     protected final static String SUBSCRIPTIONMAPPER ="SUBSCRIPTIONMAPPER";
-    
+    protected final static String ANNOTATIONMAPPER = "ANNOTATIONMAPPER";
     
     
     /**
@@ -66,6 +68,17 @@ public abstract class MailboxSessionMapperFactory implements 
RequestAware, Mailb
         return mapper;
     }
 
+    public AnnotationMapper getAnnotationMapper(MailboxId mailboxId, 
MailboxSession session) throws MailboxException {
+        AnnotationMapper mapper = 
(AnnotationMapper)session.getAttributes().get(ANNOTATIONMAPPER);
+        if (mapper == null) {
+            mapper = createAnnotationMapper(mailboxId, session);
+            session.getAttributes().put(ANNOTATIONMAPPER, mapper);
+        }
+        return mapper;
+    }
+
+    public abstract AnnotationMapper createAnnotationMapper(MailboxId 
mailboxId, MailboxSession session) throws MailboxException;
+
     /**
      * Create a {@link MessageMapper} instance which will get reused during 
the whole {@link MailboxSession}
      * 

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
----------------------------------------------------------------------
diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index 4dbb447..1d671af 100644
--- 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -25,6 +25,7 @@ import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Random;
+import java.util.Set;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
@@ -45,6 +46,7 @@ import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.MailboxExistsException;
 import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.model.MailboxACL;
+import org.apache.james.mailbox.model.MailboxAnnotation;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxMetaData;
 import org.apache.james.mailbox.model.MailboxMetaData.Selectability;
@@ -57,6 +59,7 @@ import org.apache.james.mailbox.quota.QuotaRootResolver;
 import org.apache.james.mailbox.store.event.DefaultDelegatingMailboxListener;
 import org.apache.james.mailbox.store.event.DelegatingMailboxListener;
 import org.apache.james.mailbox.store.event.MailboxEventDispatcher;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.model.Mailbox;
 import org.apache.james.mailbox.store.mail.model.impl.MessageParser;
@@ -134,7 +137,6 @@ public class StoreMailboxManager implements MailboxManager {
     public void setMailboxSessionIdGenerator(MailboxSessionIdGenerator 
idGenerator) {
         this.idGenerator = idGenerator;
     }
-
     public void setQuotaManager(QuotaManager quotaManager) {
         this.quotaManager = quotaManager;
     }
@@ -701,4 +703,42 @@ public class StoreMailboxManager implements MailboxManager 
{
         );
     }
 
+    private AnnotationMapper getAnnotationMapper(MailboxPath mailboxPath, 
MailboxSession session)
+            throws MailboxException, MailboxNotFoundException {
+        MailboxMapper mailboxMapper = 
mailboxSessionMapperFactory.getMailboxMapper(session);
+        Mailbox mailbox = mailboxMapper.findMailboxByPath(mailboxPath);
+        AnnotationMapper annotationMapper = 
mailboxSessionMapperFactory.getAnnotationMapper(mailbox.getMailboxId(), 
session);
+        return annotationMapper;
+    }
+    
+    @Override
+    public List<MailboxAnnotation> getAllAnnotations(MailboxPath mailboxPath, 
MailboxSession session) throws MailboxException {
+        AnnotationMapper annotationMapper = getAnnotationMapper(mailboxPath, 
session);
+        return annotationMapper.getAllAnnotations();
+    }
+
+    @Override
+    public List<MailboxAnnotation> getAnnotationsByKeys(MailboxPath 
mailboxPath, MailboxSession session, Set<String> keys) 
+            throws MailboxException {
+        AnnotationMapper annotationMapper = getAnnotationMapper(mailboxPath, 
session);
+        return annotationMapper.getAnnotationsByKeys(keys);
+    }
+
+    @Override
+    public void updateAnnotations(MailboxPath mailboxPath, MailboxSession 
session, List<MailboxAnnotation> mailboxAnnotations) 
+            throws MailboxException {
+        AnnotationMapper annotationMapper = getAnnotationMapper(mailboxPath, 
session);
+        for (MailboxAnnotation annotation : mailboxAnnotations) {
+            if (annotation.isNil()) {
+                annotationMapper.deleteAnnotation(annotation.getKey());
+            } else {
+                annotationMapper.insertAnnotation(annotation);
+            }
+        }
+    }
+
+    @Override
+    public boolean hasCapability(MailboxCapabilities capability) {
+        return getSupportedMailboxCapabilities().contains(capability);
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/AnnotationMapper.java
----------------------------------------------------------------------
diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/AnnotationMapper.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/AnnotationMapper.java
new file mode 100644
index 0000000..d2b6e4d
--- /dev/null
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/AnnotationMapper.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * 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.mail;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.james.mailbox.model.MailboxAnnotation;
+import org.apache.james.mailbox.store.transaction.Mapper;
+
+public interface AnnotationMapper extends Mapper {
+
+    /**
+     * Return all <code>MailboxAnnotation</code>.
+     * The result is not ordered and should not contain duplicate by key
+     * 
+     * @return List<MailboxAnnotation>
+     */
+    List<MailboxAnnotation> getAllAnnotations();
+
+    /**
+     * Search all the <code>MailboxAnnotation</code> by the set of 
annotation's keys. The result is not ordered and should not
+     * contain duplicate by key
+     * 
+     * @param keys the set of the key should be filtered
+     * @return List<MailboxAnnotation>
+     */
+    List<MailboxAnnotation> getAnnotationsByKeys(Set<String> keys);
+
+    /**
+     * Delete the annotation by its key.
+     * 
+     * @param key the key of annotation should be deleted
+     */
+    void deleteAnnotation(String key);
+
+    /**
+     * - Insert new annotation if it does not exist on store
+     * - Update the new value for existed annotation
+     * 
+     * @param mailboxAnnotation the selected annotation
+     */
+    void insertAnnotation(MailboxAnnotation mailboxAnnotation);
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/60b612ae/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
----------------------------------------------------------------------
diff --git 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
new file mode 100644
index 0000000..22efc19
--- /dev/null
+++ 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
@@ -0,0 +1,148 @@
+/****************************************************************
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.acl.GroupMembershipResolver;
+import org.apache.james.mailbox.acl.MailboxACLResolver;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxAnnotation;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.store.mail.AnnotationMapper;
+import org.apache.james.mailbox.store.mail.MailboxMapper;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
+import org.apache.james.mailbox.store.mail.model.impl.MessageParser;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+public class StoreMailboxManagerAnnotationTest {
+    private static final MailboxAnnotation PRIVATE_ANNOTATION = 
MailboxAnnotation.newInstance("/private/comment", "My private comment");
+    private static final MailboxAnnotation SHARED_ANNOTATION =  
MailboxAnnotation.newInstance("/shared/comment", "My shared comment");
+    private static final Set<String> KEYS = 
ImmutableSet.of("/private/comment");
+
+    private static final List<MailboxAnnotation> ANNOTATIONS = 
ImmutableList.of(PRIVATE_ANNOTATION, SHARED_ANNOTATION);
+    private static final List<MailboxAnnotation> ANNOTATIONS_WITH_NIL_ENTRY = 
ImmutableList.of(PRIVATE_ANNOTATION, MailboxAnnotation.nil("/shared/comment"));
+
+    @Mock private MailboxSessionMapperFactory mailboxSessionMapperFactory;
+    @Mock private Authenticator authenticator;
+    @Mock private MailboxACLResolver aclResolver;
+    @Mock private GroupMembershipResolver groupMembershipResolver;
+    
+    @Mock private MailboxSession session;
+    @Mock private MailboxMapper mailboxMapper;
+    @Mock private AnnotationMapper annotationMapper;
+    @Mock private MailboxPath mailboxPath;
+    @Mock private Mailbox mailbox;
+    @Mock private MessageParser messageParser;
+    @Mock private MailboxId mailboxId;
+
+    @InjectMocks
+    private StoreMailboxManager storeMailboxManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        
when(mailboxSessionMapperFactory.getMailboxMapper(eq(session))).thenReturn(mailboxMapper);
+        when(mailbox.getMailboxId()).thenReturn(mailboxId);
+        when(mailboxSessionMapperFactory.getAnnotationMapper(eq(mailboxId), 
eq(session))).thenReturn(annotationMapper);
+
+        storeMailboxManager = new 
StoreMailboxManager(mailboxSessionMapperFactory, authenticator, aclResolver, 
groupMembershipResolver, messageParser);
+    }
+
+    @Test(expected = MailboxException.class)
+    public void 
updateAnnotationsShouldThrowExceptionWhenDoesNotLookupMailbox() throws 
Exception {
+        
doThrow(MailboxException.class).when(mailboxMapper).findMailboxByPath(eq(mailboxPath));
+        storeMailboxManager.updateAnnotations(mailboxPath, session, 
ImmutableList.of(PRIVATE_ANNOTATION));
+    }
+
+    @Test
+    public void 
updateAnnotationsShouldCallAnnotationMapperToInsertAnnotation() throws 
Exception {
+        
when(mailboxMapper.findMailboxByPath(eq(mailboxPath))).thenReturn(mailbox);
+        storeMailboxManager.updateAnnotations(mailboxPath, session, 
ANNOTATIONS);
+
+        verify(annotationMapper, 
times(2)).insertAnnotation(any(MailboxAnnotation.class));
+    }
+
+    @Test
+    public void 
updateAnnotationsShouldCallAnnotationMapperToDeleteAnnotation() throws 
Exception {
+        
when(mailboxMapper.findMailboxByPath(eq(mailboxPath))).thenReturn(mailbox);
+        storeMailboxManager.updateAnnotations(mailboxPath, session, 
ANNOTATIONS_WITH_NIL_ENTRY);
+
+        verify(annotationMapper, 
times(1)).insertAnnotation(eq(PRIVATE_ANNOTATION));
+        verify(annotationMapper, 
times(1)).deleteAnnotation(eq("/shared/comment"));
+    }
+
+    @Test(expected = MailboxException.class)
+    public void 
getAllAnnotationsShouldThrowExceptionWhenDoesNotLookupMailbox() throws 
Exception {
+        
doThrow(MailboxException.class).when(mailboxMapper).findMailboxByPath(eq(mailboxPath));
+        storeMailboxManager.getAllAnnotations(mailboxPath, session);
+    }
+
+    @Test
+    public void getAllAnnotationsShouldReturnEmptyForNonStoredAnnotation() 
throws Exception {
+        
when(mailboxMapper.findMailboxByPath(eq(mailboxPath))).thenReturn(mailbox);
+        
when(annotationMapper.getAllAnnotations()).thenReturn(Collections.<MailboxAnnotation>
 emptyList());
+
+        assertThat(storeMailboxManager.getAllAnnotations(mailboxPath, 
session)).isEmpty();
+    }
+
+    @Test
+    public void getAllAnnotationsShouldReturnStoredAnnotation() throws 
Exception {
+        
when(mailboxMapper.findMailboxByPath(eq(mailboxPath))).thenReturn(mailbox);
+        when(annotationMapper.getAllAnnotations()).thenReturn(ANNOTATIONS);
+
+        assertThat(storeMailboxManager.getAllAnnotations(mailboxPath, 
session)).isEqualTo(ANNOTATIONS);
+    }
+
+    @Test(expected = MailboxException.class)
+    public void 
getAnnotationsByKeysShouldThrowExceptionWhenDoesNotLookupMailbox() throws 
Exception {
+        
doThrow(MailboxException.class).when(mailboxMapper).findMailboxByPath(eq(mailboxPath));
+        storeMailboxManager.getAnnotationsByKeys(mailboxPath, session, KEYS);
+    }
+
+    @Test
+    public void getAnnotationsByKeysShouldRetrieveStoreAnnotationsByKey() 
throws Exception {
+        
when(mailboxMapper.findMailboxByPath(eq(mailboxPath))).thenReturn(mailbox);
+        
when(annotationMapper.getAnnotationsByKeys(KEYS)).thenReturn(ANNOTATIONS);
+
+        assertThat(storeMailboxManager.getAnnotationsByKeys(mailboxPath, 
session, KEYS)).isEqualTo(ANNOTATIONS);
+    }
+
+}


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

Reply via email to