You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by ra...@apache.org on 2019/09/01 10:54:16 UTC

[mina-vysper] 01/01: XEP-0313 Message Archive Management

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

ralaoui pushed a commit to branch xep-0313
in repository https://gitbox.apache.org/repos/asf/mina-vysper.git

commit aff6f45a6f65d7eb4dec179c88b4540084929d80
Author: Réda Housni Alaoui <re...@gmail.com>
AuthorDate: Tue Aug 6 13:54:52 2019 +0200

    XEP-0313 Message Archive Management
---
 pom.xml                                            |   6 +
 .../vysper/xmpp/datetime/DateTimeProfile.java      |  10 +
 .../core/base/handler/AcceptedMessageEvent.java    |  75 ++++++++
 .../modules/core/base/handler/MessageHandler.java  |  21 ++-
 .../xep0059_result_set_management/Set.java         |  67 ++++---
 .../apache/vysper/xmpp/parser/XMLParserUtil.java   |   5 +
 .../apache/vysper/xmpp/protocol/NamespaceURIs.java |   4 +
 .../apache/vysper/xmpp/stanza/StanzaBuilder.java   |   4 +-
 .../xmpp/stanza/dataforms/DataFormParser.java      |   4 +-
 .../ResultSetManagementTest.java                   |  12 +-
 .../stanza/dataforms/DataFormParserTestCase.java   |   4 +-
 server/extensions/pom.xml                          |   1 +
 server/extensions/xep0313-mam/pom.xml              | 106 +++++++++++
 .../modules/extension/xep0313_mam/MAMModule.java   | 108 +++++++++++
 .../extension/xep0313_mam/SimpleMessage.java       |  57 ++++++
 .../InMemoryArchivedMessagesLastPage.java          |  70 +++++++
 .../in_memory/InMemoryArchivedMessagesPage.java    |  90 +++++++++
 .../in_memory/InMemoryDateTimeFilter.java          |  48 +++++
 .../in_memory/InMemoryEntityFilter.java            |  71 +++++++
 .../in_memory/InMemoryMessageArchive.java          |  64 +++++++
 .../in_memory/InMemoryMessageArchives.java         |  43 +++++
 .../in_memory/InMemoryMessageFilter.java           |  49 +++++
 .../xep0313_mam/muc/MUCArchiveQueryHandler.java    |  57 ++++++
 .../pubsub/PubsubNodeArchiveQueryHandler.java      |  53 ++++++
 .../xep0313_mam/query/ArchiveEntityFilter.java     |  79 ++++++++
 .../extension/xep0313_mam/query/ArchiveFilter.java |  52 ++++++
 .../extension/xep0313_mam/query/ArchiveQuery.java  |  59 ++++++
 .../extension/xep0313_mam/query/MAMIQHandler.java  |  88 +++++++++
 .../query/MatchingArchivedMessageResult.java       | 108 +++++++++++
 .../query/MatchingArchivedMessageResults.java      |  93 ++++++++++
 .../modules/extension/xep0313_mam/query/Query.java |  88 +++++++++
 .../extension/xep0313_mam/query/QueryHandler.java  |  37 ++++
 .../extension/xep0313_mam/query/QuerySet.java      |  78 ++++++++
 .../modules/extension/xep0313_mam/query/X.java     |  83 +++++++++
 .../extension/xep0313_mam/spi/ArchivedMessage.java |  34 ++++
 .../xep0313_mam/spi/ArchivedMessages.java          |  60 ++++++
 .../extension/xep0313_mam/spi/DateTimeFilter.java  |  48 +++++
 .../extension/xep0313_mam/spi/EntityFilter.java    |  56 ++++++
 .../modules/extension/xep0313_mam/spi/Message.java |  35 ++++
 .../extension/xep0313_mam/spi/MessageArchive.java  |  43 +++++
 .../extension/xep0313_mam/spi/MessageArchives.java |  34 ++++
 .../extension/xep0313_mam/spi/MessageFilter.java   |  33 ++++
 .../xep0313_mam/spi/MessagePageRequest.java        |  35 ++++
 .../xep0313_mam/spi/SimpleArchivedMessage.java     |  82 +++++++++
 .../xep0313_mam/spi/SimpleArchivedMessages.java    |  82 +++++++++
 .../xep0313_mam/user/UserArchiveQueryHandler.java  |  93 ++++++++++
 .../xep0313_mam/user/UserMessageListener.java      |  93 ++++++++++
 .../extension/xep0313_mam/IntegrationTest.java     | 203 +++++++++++++++++++++
 .../extension/xep0313_mam/MAMModuleTest.java       |  84 +++++++++
 .../xep0313_mam/ServerRuntimeContextMock.java      | 158 ++++++++++++++++
 .../extension/xep0313_mam/SessionContextMock.java  | 145 +++++++++++++++
 .../extension/xep0313_mam/StanzaAssert.java        |  37 ++++
 .../extension/xep0313_mam/UserArchiveTest.java     | 149 +++++++++++++++
 .../in_memory/InMemoryEntityFilterTest.java        |  99 ++++++++++
 .../InMemoryPageLimitedArchivedMessagesTest.java   | 160 ++++++++++++++++
 .../xep0313_mam/query/ArchiveEntityFilterTest.java |  68 +++++++
 .../xep0313_mam/query/ArchiveQueryTest.java        |  85 +++++++++
 .../xep0313_mam/query/MAMIQHandlerTest.java        | 140 ++++++++++++++
 .../query/MatchingArchivedMessageResultsTest.java  | 181 ++++++++++++++++++
 .../extension/xep0313_mam/query/QuerySetTest.java  |  58 ++++++
 .../xep0313_mam/spi/MessageArchiveMock.java        |  65 +++++++
 .../xep0313_mam/spi/MessageArchivesMock.java       |  45 +++++
 .../xep0313_mam/spi/SimpleEntityFilter.java        |  57 ++++++
 .../xep0313_mam/spi/SimpleMessagePageRequest.java  |  58 ++++++
 .../user/UserArchiveQueryHandlerTest.java          | 124 +++++++++++++
 .../xep0313_mam/user/UserMessageListenerTest.java  | 155 ++++++++++++++++
 .../src/test/resources/bogus_mina_tls.cert         | Bin 0 -> 2623 bytes
 67 files changed, 4553 insertions(+), 40 deletions(-)

diff --git a/pom.xml b/pom.xml
index b3470ee..5281b32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -291,6 +291,12 @@
       </dependency>
 
       <dependency>
+        <groupId>org.igniterealtime.smack</groupId>
+        <artifactId>smack-experimental</artifactId>
+        <version>4.3.4</version>
+      </dependency>
+
+      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-log4j12</artifactId>
         <version>1.6.1</version>
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java b/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
index 21377b4..aefe644 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
@@ -22,6 +22,7 @@ package org.apache.vysper.xmpp.datetime;
 import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.COMPLETE;
 import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.IN_PROGRESS;
 
+import java.time.ZonedDateTime;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.TimeZone;
@@ -80,6 +81,10 @@ public class DateTimeProfile {
     public String getDateTimeInUTC(Date time) {
         return utcDateTimeFormatter.format(time);
     }
+    
+    public String getDateTimeInUTC(ZonedDateTime dateTime){
+        return getDateTimeInUTC(Date.from(dateTime.toInstant()));
+    }
 
     public String getDateInUTC(Date time) {
         return utcDateFormatter.format(time);
@@ -122,6 +127,11 @@ public class DateTimeProfile {
             throw new IllegalArgumentException("Invalid date time: " + time);
         }
     }
+    
+    public ZonedDateTime fromZonedDateTime(String time){
+        Calendar calendar = fromDateTime(time);
+        return ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
+    }
 
     /**
      * Parses a time, compliant with ISO-8601 and XEP-0082.
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java
new file mode 100644
index 0000000..a81426c
--- /dev/null
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java
@@ -0,0 +1,75 @@
+/*
+ *  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.vysper.xmpp.modules.core.base.handler;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class AcceptedMessageEvent {
+
+    private final ServerRuntimeContext serverRuntimeContext;
+
+    private final SessionContext sessionContext;
+
+    private final boolean outbound;
+
+    private final MessageStanza messageStanza;
+
+    public AcceptedMessageEvent(ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext,
+            boolean outbound, MessageStanza messageStanza) {
+        this.serverRuntimeContext = requireNonNull(serverRuntimeContext);
+        this.sessionContext = requireNonNull(sessionContext);
+        this.outbound = outbound;
+        this.messageStanza = requireNonNull(messageStanza);
+    }
+
+    public ServerRuntimeContext serverRuntimeContext() {
+        return serverRuntimeContext;
+    }
+
+    public SessionContext sessionContext() {
+        return sessionContext;
+    }
+
+    public boolean isOutbound() {
+        return outbound;
+    }
+
+    public MessageStanza messageStanza() {
+        return messageStanza;
+    }
+    
+    @Override
+    public String toString() {
+        return "AcceptedMessageEvent{" + "serverRuntimeContext=" + serverRuntimeContext + ", sessionContext="
+                + sessionContext + ", outbound=" + outbound + ", messageStanza=" + messageStanza + '}';
+    }
+    
+    
+}
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
index 4958685..639a230 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
@@ -22,6 +22,7 @@ package org.apache.vysper.xmpp.modules.core.base.handler;
 
 import java.util.List;
 
+import org.apache.vysper.event.EventBus;
 import org.apache.vysper.xml.fragment.Attribute;
 import org.apache.vysper.xml.fragment.XMLElement;
 import org.apache.vysper.xml.fragment.XMLSemanticError;
@@ -42,6 +43,7 @@ import org.apache.vysper.xmpp.stanza.XMPPCoreStanza;
  * @author The Apache MINA Project (dev@mina.apache.org)
  */
 public class MessageHandler extends XMPPCoreStanzaHandler {
+
     public String getName() {
         return "message";
     }
@@ -89,13 +91,8 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
 
         // TODO inspect all BODY elements and make sure they conform to the spec
 
+        EventBus eventBus = serverRuntimeContext.getEventBus();
         if (isOutboundStanza) {
-            // check if message reception is turned of either globally or locally
-            if (!serverRuntimeContext.getServerFeatures().isRelayingMessages()
-                    || (sessionContext != null && sessionContext
-                            .getAttribute(SessionContext.SESSION_ATTRIBUTE_MESSAGE_STANZA_NO_RECEIVE) != null)) {
-                return null;
-            }
 
             Entity from = stanza.getFrom();
             if (from == null || !from.isResourceSet()) {
@@ -118,6 +115,16 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
                 stanza = XMPPCoreStanza.getWrapper(stanzaBuilder.build());
             }
 
+            eventBus.publish(AcceptedMessageEvent.class,
+                    new AcceptedMessageEvent(serverRuntimeContext, sessionContext, true, new MessageStanza(stanza)));
+
+            // check if message reception is turned of either globally or locally
+            if (!serverRuntimeContext.getServerFeatures().isRelayingMessages()
+                    || (sessionContext != null && sessionContext
+                            .getAttribute(SessionContext.SESSION_ATTRIBUTE_MESSAGE_STANZA_NO_RECEIVE) != null)) {
+                return null;
+            }
+
             try {
                 stanzaBroker.write(stanza.getTo(), stanza, new ReturnErrorToSenderFailureStrategy(stanzaBroker));
             } catch (Exception e) {
@@ -125,6 +132,8 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
                 e.printStackTrace(); // To change body of catch statement use File | Settings | File Templates.
             }
         } else {
+			eventBus.publish(AcceptedMessageEvent.class,
+					new AcceptedMessageEvent(serverRuntimeContext, sessionContext, false, new MessageStanza(stanza)));
             stanzaBroker.writeToSession(stanza);
         }
         return null;
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
index 680ce68..c6da782 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
@@ -26,7 +26,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.OptionalInt;
 
 import org.apache.vysper.xml.fragment.Attribute;
 import org.apache.vysper.xml.fragment.XMLElement;
@@ -44,7 +43,7 @@ import org.apache.vysper.xmpp.protocol.NamespaceURIs;
  */
 public class Set {
 
-    private static final String ELEMENT_NAME = "set";
+    public static final String ELEMENT_NAME = "set";
 
     private static final String FIRST = "first";
 
@@ -64,6 +63,15 @@ public class Set {
         this.element = element;
     }
 
+    public static Set empty() {
+        return new Set(new XMLElement(NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT, ELEMENT_NAME, null,
+                Collections.emptyList(), Collections.emptyList()));
+    }
+
+    public XMLElement element() {
+        return element;
+    }
+
     public static Builder builder() {
         return new Builder();
     }
@@ -76,33 +84,47 @@ public class Set {
         return getElementText("before");
     }
 
-    public OptionalInt getCount() throws XMLSemanticError {
-        return getElementInt("count");
+    public Optional<Long> getCount() throws XMLSemanticError {
+        return getElementLong("count");
     }
 
     public Optional<First> getFirst() throws XMLSemanticError {
         return ofNullable(element.getSingleInnerElementsNamed(FIRST)).map(First::new);
     }
 
-    public OptionalInt getIndex() throws XMLSemanticError {
-        return getElementInt(INDEX);
+    public Optional<Long> getIndex() throws XMLSemanticError {
+        return getElementLong(INDEX);
     }
 
     public Optional<String> getLast() throws XMLSemanticError {
         return getElementText("last");
     }
 
-    public OptionalInt getMax() throws XMLSemanticError {
-        return getElementInt("max");
+    public Optional<Long> getMax() throws XMLSemanticError {
+        return getElementLong("max");
     }
 
     private Optional<String> getElementText(String elementName) throws XMLSemanticError {
-        return ofNullable(element.getSingleInnerElementsNamed(elementName)).map(XMLElement::getInnerText)
-                .map(XMLText::getText);
+        XMLElement xmlElement = element.getSingleInnerElementsNamed(elementName);
+        if (xmlElement == null) {
+            return Optional.empty();
+        }
+
+        XMLText xmlText = xmlElement.getInnerText();
+        if (xmlText == null) {
+            return Optional.of("");
+        }
+
+        String text = xmlText.getText();
+        if (text == null) {
+            return Optional.of("");
+        }
+
+        return Optional.of(text);
     }
 
-    private OptionalInt getElementInt(String elementName) throws XMLSemanticError {
-        return getElementText(elementName).map(Integer::parseInt).map(OptionalInt::of).orElse(OptionalInt.empty());
+    private Optional<Long> getElementLong(String elementName) throws XMLSemanticError {
+        return getElementText(elementName).map(Long::parseLong);
     }
 
     public static class First {
@@ -121,9 +143,8 @@ public class Set {
             return element.getInnerText().getText();
         }
 
-        public OptionalInt getIndex() {
-            return ofNullable(element.getAttributeValue(INDEX)).map(Integer::parseInt).map(OptionalInt::of)
-                    .orElse(OptionalInt.empty());
+        public Optional<Long> getIndex() {
+            return ofNullable(element.getAttributeValue(INDEX)).map(Long::parseLong);
         }
 
     }
@@ -134,15 +155,15 @@ public class Set {
 
         private String before;
 
-        private Integer count;
+        private Long count;
 
         private First first;
 
-        private Integer index;
+        private Long index;
 
         private String last;
 
-        private Integer max;
+        private Long max;
 
         private Builder() {
 
@@ -158,7 +179,7 @@ public class Set {
             return this;
         }
 
-        public Builder count(Integer count) {
+        public Builder count(Long count) {
             this.count = count;
             return this;
         }
@@ -167,7 +188,7 @@ public class Set {
             return new FirstBuilder(this);
         }
 
-        public Builder index(Integer index) {
+        public Builder index(Long index) {
             this.index = index;
             return this;
         }
@@ -177,7 +198,7 @@ public class Set {
             return this;
         }
 
-        public Builder max(Integer max) {
+        public Builder max(Long max) {
             this.max = max;
             return this;
         }
@@ -209,7 +230,7 @@ public class Set {
 
         private String value;
 
-        private Integer index;
+        private Long index;
 
         private FirstBuilder(Builder builder) {
             this.builder = requireNonNull(builder);
@@ -220,7 +241,7 @@ public class Set {
             return this;
         }
 
-        public FirstBuilder index(Integer index) {
+        public FirstBuilder index(Long index) {
             this.index = index;
             return this;
         }
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java b/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
index 1487ca0..cd59af2 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
@@ -33,6 +33,8 @@ import org.apache.vysper.xml.sax.NonBlockingXMLReader;
 import org.apache.vysper.xml.sax.impl.DefaultNonBlockingXMLReader;
 import org.xml.sax.SAXException;
 
+import static java.util.Optional.ofNullable;
+
 /**
  *
  * @author The Apache MINA Project (dev@mina.apache.org)
@@ -73,5 +75,8 @@ public class XMLParserUtil {
         }
     }
 
+    public static XMLElement parseRequiredDocument(String xml) throws IOException, SAXException {
+        return ofNullable(parseDocument(xml)).orElseThrow(() -> new IllegalStateException("Parsed element should not be null"));
+    }
     
 }
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java b/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
index 56bc78c..d86a161 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
@@ -111,4 +111,8 @@ public class NamespaceURIs {
     public static final String XEP0133_SERVICE_ADMIN = "http://jabber.org/protocol/admin";
     
     public static final String XEP0059_RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
+    
+    public static final String XEP0297_STANZA_FORWARDING = "urn:xmpp:forward:0";
+
+    public static final String XEP0359_STANZA_IDS = "urn:xmpp:sid:0";
 }
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
index da697a1..fb28a97 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
@@ -55,7 +55,9 @@ public class StanzaBuilder extends AbstractXMLElementBuilder<StanzaBuilder, Stan
 
     public static StanzaBuilder createMessageStanza(Entity from, Entity to, String lang, String body) {
         StanzaBuilder stanzaBuilder = new StanzaBuilder("message", NamespaceURIs.JABBER_CLIENT);
-        stanzaBuilder.addAttribute("from", from.getFullQualifiedName());
+        if (from != null) {
+            stanzaBuilder.addAttribute("from", from.getFullQualifiedName());
+        }
         stanzaBuilder.addAttribute("to", to.getFullQualifiedName());
         if (lang != null)
             stanzaBuilder.addAttribute(NamespaceURIs.XML, "lang", lang);
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
index 0682bca..6efd971 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
@@ -76,7 +76,7 @@ public class DataFormParser {
     public Map<String, Object> extractFieldValues() throws IllegalArgumentException {
         Map<String,Object> map = new LinkedHashMap<String, Object>();
 
-        for (XMLElement fields : form.getInnerElementsNamed("field", NamespaceURIs.JABBER_X_DATA)) {
+        for (XMLElement fields : form.getInnerElementsNamed("field")) {
             final String varName = fields.getAttributeValue("var");
             final String typeName = fields.getAttributeValue("type");
             String valueAsString = null;
@@ -93,7 +93,7 @@ public class DataFormParser {
             boolean isMulti = Field.Type.isMulti(fieldType);
 
             List<Object> values = isMulti ? new ArrayList<Object>() : null;
-            for (XMLElement valueCandidates : fields.getInnerElementsNamed("value", NamespaceURIs.JABBER_X_DATA)) {
+            for (XMLElement valueCandidates : fields.getInnerElementsNamed("value")) {
                 final XMLText firstInnerText = valueCandidates.getFirstInnerText();
                 if (firstInnerText != null) valueAsString = firstInnerText.getText();
                 Object value;
diff --git a/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java b/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
index c52dfd6..5049e14 100644
--- a/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
+++ b/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
@@ -31,19 +31,19 @@ public class ResultSetManagementTest {
 
     @Test
     public void parse() throws XMLSemanticError {
-        Set tested = Set.builder().after("after-value").before("before-value").count(5)
-                .index(10).last("last-value").max(15).startFirst().index(20).value("first-value").endFirst().build();
+        Set tested = Set.builder().after("after-value").before("before-value").count(5L).index(10L).last("last-value")
+                .max(15L).startFirst().index(20L).value("first-value").endFirst().build();
 
         assertEquals("after-value", tested.getAfter().get());
         assertEquals("before-value", tested.getBefore().get());
-        assertEquals(5, tested.getCount().getAsInt());
-        assertEquals(10, tested.getIndex().getAsInt());
+        assertEquals(5L, (long) tested.getCount().orElse(0L));
+        assertEquals(10L, (long) tested.getIndex().orElse(0L));
         assertEquals("last-value", tested.getLast().get());
-        assertEquals(15, tested.getMax().getAsInt());
+        assertEquals(15L, (long) tested.getMax().orElse(0L));
 
         Set.First first = tested.getFirst().get();
         assertEquals("first-value", first.getValue());
-        assertEquals(20, first.getIndex().getAsInt());
+        assertEquals(20L, (long) first.getIndex().orElse(0L));
     }
 
 }
\ No newline at end of file
diff --git a/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java b/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
index 650ce83..a3c92a0 100644
--- a/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
+++ b/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
@@ -134,14 +134,14 @@ public class DataFormParserTestCase {
 
     private void assertMultiValue(String type, List<String> values, List<Object> expectedValues) {
         XMLElementBuilder builder = new XMLElementBuilder("x", NamespaceURIs.JABBER_X_DATA)
-        .startInnerElement("field", NamespaceURIs.JABBER_X_DATA)
+        .startInnerElement("field")
         .addAttribute("var", "fie1");
         
         if(type != null) builder.addAttribute("type", type);
         
         
         for(String value : values) {
-            builder.startInnerElement("value", NamespaceURIs.JABBER_X_DATA);
+            builder.startInnerElement("value");
             builder.addText(value);
             builder.endInnerElement();
         }
diff --git a/server/extensions/pom.xml b/server/extensions/pom.xml
index f785d50..e11e83b 100644
--- a/server/extensions/pom.xml
+++ b/server/extensions/pom.xml
@@ -37,6 +37,7 @@
     <module>xep0065-socks</module>
     <module>xep0124-xep0206-bosh</module>
     <module>websockets</module>
+    <module>xep0313-mam</module>
   </modules>
 
 </project>
diff --git a/server/extensions/xep0313-mam/pom.xml b/server/extensions/xep0313-mam/pom.xml
new file mode 100644
index 0000000..12314c4
--- /dev/null
+++ b/server/extensions/xep0313-mam/pom.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+  <!--
+    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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <parent>
+    <artifactId>vysper-extensions</artifactId>
+    <groupId>org.apache.vysper</groupId>
+    <version>0.8-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.vysper.extensions</groupId>
+  <artifactId>xep0313-mam</artifactId>
+  <name>Apache Vysper XEP-0313 Message Archive Management</name>
+  <version>0.8-SNAPSHOT</version>
+  
+  
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.vysper</groupId>
+      <artifactId>spec-compliance</artifactId>
+      <optional>true</optional>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.vysper</groupId>
+      <artifactId>vysper-core</artifactId>
+    </dependency>
+    
+    <!-- Runtime dependencies -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+    
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+      
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-tcp</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-resolver-javax</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-sasl-javax</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-java7</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-experimental</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.igniterealtime.smack</groupId>
+      <artifactId>smack-extensions</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java
new file mode 100644
index 0000000..b3c3ece
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java
@@ -0,0 +1,108 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.vysper.event.EventListenerDictionary;
+import org.apache.vysper.event.SimpleEventListenerDictionary;
+import org.apache.vysper.xmpp.modules.DefaultDiscoAwareModule;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.MAMIQHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserMessageListener;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.Feature;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoElement;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequest;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServerInfoRequestListener;
+import org.apache.vysper.xmpp.protocol.HandlerDictionary;
+import org.apache.vysper.xmpp.protocol.NamespaceHandlerDictionary;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+
+/**
+ * A module for <a href="https://xmpp.org/extensions/xep-0313.html">XEP-0313
+ * Message Archive Management</a>
+ * 
+ * @author Réda Housni Alaoui
+ */
+public class MAMModule extends DefaultDiscoAwareModule implements ServerInfoRequestListener {
+
+    private static final String NAMESPACE_V1 = "urn:xmpp:mam:1";
+
+    private static final String NAMESPACE_V2 = "urn:xmpp:mam:2";
+
+    private final UserMessageListener userMessageListener = new UserMessageListener();
+
+    @Override
+    public void initialize(ServerRuntimeContext serverRuntimeContext) {
+        super.initialize(serverRuntimeContext);
+
+        requireNonNull((MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+                "Could not find an instance of " + MessageArchives.class);
+    }
+
+    @Override
+    public String getName() {
+        return "XEP-0313 Message Archive Management";
+    }
+
+    @Override
+    public String getVersion() {
+        return "0.6.3";
+    }
+
+    @Override
+    protected void addServerInfoRequestListeners(List<ServerInfoRequestListener> serverInfoRequestListeners) {
+        serverInfoRequestListeners.add(this);
+    }
+
+    @Override
+    public List<InfoElement> getServerInfosFor(InfoRequest request) {
+        if (StringUtils.isNotEmpty(request.getNode())) {
+            return null;
+        }
+
+        List<InfoElement> infoElements = new ArrayList<>();
+        infoElements.add(new Feature(NAMESPACE_V1));
+        infoElements.add(new Feature(NAMESPACE_V2));
+        infoElements.add(new Feature(NamespaceURIs.XEP0359_STANZA_IDS));
+        infoElements.add(new Feature(NamespaceURIs.JABBER_X_DATA));
+        return infoElements;
+    }
+
+    @Override
+    protected void addHandlerDictionaries(List<HandlerDictionary> dictionary) {
+        dictionary.add(new NamespaceHandlerDictionary(NAMESPACE_V1, new MAMIQHandler(NAMESPACE_V1)));
+        dictionary.add(new NamespaceHandlerDictionary(NAMESPACE_V2, new MAMIQHandler(NAMESPACE_V2)));
+    }
+
+    @Override
+    public Optional<EventListenerDictionary> getEventListenerDictionary() {
+        EventListenerDictionary dictionary = SimpleEventListenerDictionary.builder()
+                .register(AcceptedMessageEvent.class, userMessageListener).build();
+        return Optional.of(dictionary);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java
new file mode 100644
index 0000000..9e3a42b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java
@@ -0,0 +1,57 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.Message;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleMessage implements Message {
+
+    private final MessageStanza stanza;
+
+    private final ZonedDateTime dateTime;
+
+    public SimpleMessage(MessageStanza stanza) {
+        this.stanza = requireNonNull(stanza);
+        this.dateTime = ZonedDateTime.now();
+    }
+
+    @Override
+    public MessageStanza stanza() {
+        return stanza;
+    }
+
+    @Override
+    public ZonedDateTime dateTime() {
+        return dateTime;
+    }
+
+    @Override
+    public String toString() {
+        return "SimpleMessage{" + "stanza=" + stanza + ", dateTime=" + dateTime + '}';
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java
new file mode 100644
index 0000000..e7e49df
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java
@@ -0,0 +1,70 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryArchivedMessagesLastPage implements ArchivedMessages {
+
+    private final List<ArchivedMessage> list;
+
+    private final Long firstMessageIndex;
+
+    private final long totalNumberOfMessages;
+
+    public InMemoryArchivedMessagesLastPage(long pageSize, List<ArchivedMessage> unlimitedMessages) {
+        totalNumberOfMessages = unlimitedMessages.size();
+        this.firstMessageIndex = totalNumberOfMessages - Math.min(totalNumberOfMessages, pageSize);
+        this.list = unlimitedMessages.stream().skip(firstMessageIndex).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<ArchivedMessage> list() {
+        return list;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return list.isEmpty();
+    }
+
+    @Override
+    public boolean isComplete() {
+        return list.size() == totalNumberOfMessages;
+    }
+
+    @Override
+    public Optional<Long> firstMessageIndex() {
+        return Optional.ofNullable(firstMessageIndex);
+    }
+
+    @Override
+    public Optional<Long> totalNumberOfMessages() {
+        return Optional.of(totalNumberOfMessages);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java
new file mode 100644
index 0000000..3f5ed19
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java
@@ -0,0 +1,90 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryArchivedMessagesPage implements ArchivedMessages {
+
+    private final List<ArchivedMessage> list;
+
+    private final Long firstMessageIndex;
+
+    private final long totalNumberOfMessages;
+
+    public InMemoryArchivedMessagesPage(MessagePageRequest request, List<ArchivedMessage> unlimitedMessages) {
+        totalNumberOfMessages = unlimitedMessages.size();
+
+        String firstMessageId = request.firstMessageId().orElse(null);
+        String lastMessageId = request.lastMessageId().orElse(null);
+        Long pageSize = request.pageSize().orElse(null);
+
+        AtomicBoolean firstMessageFound = new AtomicBoolean(firstMessageId == null);
+        AtomicBoolean lastMessageFound = new AtomicBoolean();
+        AtomicLong numberOfMessageKept = new AtomicLong();
+
+        list = unlimitedMessages.stream().filter(message -> pageSize == null || numberOfMessageKept.get() < pageSize)
+                .peek(message -> lastMessageFound.compareAndSet(false,
+                        lastMessageId != null && lastMessageId.equals(message.id())))
+                .filter(message -> !lastMessageFound.get())
+                .peek(message -> firstMessageFound.compareAndSet(false, message.id().equals(firstMessageId)))
+                .filter(message -> firstMessageFound.get() && !message.id().equals(firstMessageId))
+                .peek(message -> numberOfMessageKept.incrementAndGet()).collect(Collectors.toList());
+
+        firstMessageIndex = list.stream().findFirst().map(unlimitedMessages::indexOf).map(index -> (long) index)
+                .orElse(null);
+    }
+    
+    @Override
+    public List<ArchivedMessage> list() {
+        return list;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return list.isEmpty();
+    }
+
+    @Override
+    public boolean isComplete() {
+        return list.size() == totalNumberOfMessages;
+    }
+
+    @Override
+    public Optional<Long> firstMessageIndex() {
+        return Optional.ofNullable(firstMessageIndex);
+    }
+
+    @Override
+    public Optional<Long> totalNumberOfMessages() {
+        return Optional.of(totalNumberOfMessages);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java
new file mode 100644
index 0000000..2b55f5c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java
@@ -0,0 +1,48 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryDateTimeFilter implements Predicate<ArchivedMessage> {
+
+    private final DateTimeFilter dateTimeFilter;
+
+    public InMemoryDateTimeFilter(DateTimeFilter dateTimeFilter) {
+        this.dateTimeFilter = requireNonNull(dateTimeFilter);
+    }
+
+    @Override
+    public boolean test(ArchivedMessage message) {
+        return Optional.of(message)
+                .filter(msg -> dateTimeFilter.start().map(start -> msg.dateTime().isAfter(start)).orElse(true))
+                .filter(msg -> dateTimeFilter.end().map(end -> msg.dateTime().isBefore(end)).orElse(true)).isPresent();
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java
new file mode 100644
index 0000000..ef0b137
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java
@@ -0,0 +1,71 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.function.Predicate;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryEntityFilter implements Predicate<ArchivedMessage> {
+
+    private final EntityFilter filter;
+
+    public InMemoryEntityFilter(EntityFilter filter) {
+        this.filter = requireNonNull(filter);
+    }
+
+    @Override
+    public boolean test(ArchivedMessage message) {
+        EntityFilter.Type type = filter.type();
+        if (type == EntityFilter.Type.TO_AND_FROM) {
+            return matchToAndFrom(message);
+        } else if (type == EntityFilter.Type.TO_OR_FROM) {
+            return matchToOrFrom(message);
+        } else {
+            throw new IllegalArgumentException("Unexpected entity filter type '" + type + "'");
+        }
+    }
+
+    private boolean matchToAndFrom(ArchivedMessage message) {
+        MessageStanza stanza = message.stanza();
+        return entitiesEquals(filter.entity(), stanza.getTo()) && entitiesEquals(filter.entity(), stanza.getFrom());
+    }
+
+    private boolean matchToOrFrom(ArchivedMessage message) {
+        MessageStanza stanza = message.stanza();
+        return entitiesEquals(filter.entity(), stanza.getTo()) || entitiesEquals(filter.entity(), stanza.getFrom());
+    }
+
+    private boolean entitiesEquals(Entity entity1, Entity entity2) {
+        if (filter.ignoreResource()) {
+            return entity1.getBareJID().equals(entity2.getBareJID());
+        } else {
+            return entity1.equals(entity2);
+        }
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java
new file mode 100644
index 0000000..c0bcc91
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java
@@ -0,0 +1,64 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.Message;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessage;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageArchive implements MessageArchive {
+
+    private final List<SimpleArchivedMessage> messages = new ArrayList<>();
+
+    @Override
+    public void archive(Message message) {
+        messages.add(new SimpleArchivedMessage(UUID.randomUUID().toString(), message));
+    }
+
+    @Override
+    public ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest) {
+        List<ArchivedMessage> filteredMessages = filterMessages(new InMemoryMessageFilter(messageFilter));
+        return new InMemoryArchivedMessagesPage(pageRequest, filteredMessages);
+    }
+
+    @Override
+    public ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize) {
+        List<ArchivedMessage> filteredMessages = filterMessages(new InMemoryMessageFilter(messageFilter));
+        return new InMemoryArchivedMessagesLastPage(pageSize, filteredMessages);
+    }
+
+    private List<ArchivedMessage> filterMessages(Predicate<ArchivedMessage> predicate) {
+        return messages.stream().filter(predicate).collect(Collectors.toList());
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java
new file mode 100644
index 0000000..52b35da
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java
@@ -0,0 +1,43 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageArchives implements MessageArchives {
+
+    private final Map<Entity, MessageArchive> userMessageArchiveById = new HashMap<>();
+
+    @Override
+    public Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid) {
+        MessageArchive messageArchive = userMessageArchiveById.computeIfAbsent(userBareJid,
+                id -> new InMemoryMessageArchive());
+        return Optional.of(messageArchive);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java
new file mode 100644
index 0000000..5f8f1cf
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java
@@ -0,0 +1,49 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageFilter implements Predicate<ArchivedMessage> {
+
+    private final Predicate<ArchivedMessage> predicate;
+
+    public InMemoryMessageFilter(MessageFilter messageFilter) {
+        Optional<Predicate<ArchivedMessage>> dateTimeFilter = messageFilter.dateTimeFilter()
+                .map(InMemoryDateTimeFilter::new);
+        Optional<Predicate<ArchivedMessage>> entityFilter = messageFilter.entityFilter().map(InMemoryEntityFilter::new);
+
+        predicate = Stream.of(dateTimeFilter, entityFilter).filter(Optional::isPresent).map(Optional::get)
+                .reduce(Predicate::and).orElse(message -> true);
+    }
+
+    @Override
+    public boolean test(ArchivedMessage message) {
+        return predicate.test(message);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java
new file mode 100644
index 0000000..7ecd3ff
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java
@@ -0,0 +1,57 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.muc;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MUCArchiveQueryHandler implements QueryHandler {
+
+    @Override
+    public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+        Entity queriedEntity = ofNullable(query.iqStanza().getTo()).orElse(sessionContext.getInitiatingEntity());
+        return !serverRuntimeContext.getServerEntity().getDomain().equals(queriedEntity.getDomain());
+    }
+
+    @Override
+    public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext,
+                               SessionContext sessionContext) {
+        // MUC archives is not yet implemented
+        Stanza notImplemented = ServerErrorResponses.getStanzaError(StanzaErrorCondition.FEATURE_NOT_IMPLEMENTED,
+                query.iqStanza(), StanzaErrorType.MODIFY,
+                "Multi-User Chat message archive feature is not yet implemented", null, null);
+        return Collections.singletonList(notImplemented);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java
new file mode 100644
index 0000000..be88bbb
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java
@@ -0,0 +1,53 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class PubsubNodeArchiveQueryHandler implements QueryHandler {
+
+    @Override
+    public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+        return query.getNode().isPresent();
+    }
+
+    @Override
+    public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext,
+                               SessionContext sessionContext) {
+        // PubSub node archives is not yet implemented
+        Stanza notImplemented = ServerErrorResponses.getStanzaError(StanzaErrorCondition.FEATURE_NOT_IMPLEMENTED,
+                query.iqStanza(), StanzaErrorType.CANCEL, "Pubsub node message archive feature is not yet implemented",
+                null, null);
+        return Collections.singletonList(notImplemented);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java
new file mode 100644
index 0000000..c17603e
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java
@@ -0,0 +1,79 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveEntityFilter implements EntityFilter {
+
+    private final Entity with;
+
+    private final Type type;
+
+    private final boolean ignoreResource;
+
+    public ArchiveEntityFilter(Entity archiveId, Entity with) {
+        this.with = requireNonNull(with);
+        if (archiveId.equals(with)) {
+            // If the 'with' field's value is the bare JID of the archive, the server must
+            // only return results where both
+            // the 'to' and 'from' match the bare JID (either as bare or by ignoring the
+            // resource), as otherwise every
+            // message in the archive would match
+            this.type = Type.TO_AND_FROM;
+            this.ignoreResource = true;
+        } else if (!with.isResourceSet()) {
+            // If (and only if) the supplied JID is a bare JID (i.e. no resource is
+            // present),
+            // then the server SHOULD return messages if their bare to/from address for a
+            // user archive would match it.
+            this.type = Type.TO_OR_FROM;
+            this.ignoreResource = true;
+        } else {
+            // A message in a user's archive matches if the JID matches either the to or
+            // from of the message.
+            this.type = Type.TO_OR_FROM;
+            this.ignoreResource = false;
+        }
+    }
+
+    @Override
+    public Entity entity() {
+        return with;
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public boolean ignoreResource() {
+        return ignoreResource;
+    }
+    
+    
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java
new file mode 100644
index 0000000..db8fed8
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java
@@ -0,0 +1,52 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveFilter implements MessageFilter {
+
+    private final EntityFilter entityFilter;
+
+    private final DateTimeFilter dateTimeFilter;
+
+    public ArchiveFilter(Entity archiveId, X x) {
+        entityFilter = x.getWith().map(entity -> new ArchiveEntityFilter(archiveId, entity)).orElse(null);
+        dateTimeFilter = x;
+    }
+
+    @Override
+    public Optional<EntityFilter> entityFilter() {
+        return Optional.ofNullable(entityFilter);
+    }
+
+    @Override
+    public Optional<DateTimeFilter> dateTimeFilter() {
+        return Optional.of(dateTimeFilter);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java
new file mode 100644
index 0000000..7b4fde6
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java
@@ -0,0 +1,59 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveQuery {
+
+    private final MessageArchive archive;
+
+    private final Entity archiveId;
+
+    private final Query query;
+
+    public ArchiveQuery(MessageArchive archive, Entity archiveId, Query query) {
+        this.archive = requireNonNull(archive);
+        this.archiveId = requireNonNull(archiveId);
+        this.query = requireNonNull(query);
+    }
+
+    public ArchivedMessages execute() throws XMLSemanticError {
+        ArchiveFilter archiveFilter = new ArchiveFilter(archiveId, query.getX());
+        QuerySet querySet = query.getSet();
+
+        if (querySet.lastPage()) {
+            long pageSize = querySet.pageSize().orElseThrow(
+                    () -> new IllegalArgumentException("Page size must be defined when requesting last page"));
+            return archive.fetchLastPageSortedByOldestFirst(archiveFilter, pageSize);
+        } else {
+            return archive.fetchSortedByOldestFirst(archiveFilter, querySet);
+        }
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java
new file mode 100644
index 0000000..53ec4ca
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java
@@ -0,0 +1,88 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.core.base.handler.DefaultIQHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.muc.MUCArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.pubsub.PubsubNodeArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserArchiveQueryHandler;
+import org.apache.vysper.xmpp.protocol.StanzaBroker;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MAMIQHandler extends DefaultIQHandler {
+
+    private final String namespace;
+
+    private final List<QueryHandler> queryHandlers;
+
+    public MAMIQHandler(String namespace) {
+        this(namespace, new PubsubNodeArchiveQueryHandler(), new MUCArchiveQueryHandler(),
+                new UserArchiveQueryHandler());
+    }
+
+    public MAMIQHandler(String namespace, QueryHandler pubsubNodeArchiveQueryHandler,
+            QueryHandler mucArchiveQueryHandler, QueryHandler userArchiveQueryHandler) {
+        this.namespace = requireNonNull(namespace);
+        List<QueryHandler> queryHandlers = new ArrayList<>();
+        queryHandlers.add(pubsubNodeArchiveQueryHandler);
+        queryHandlers.add(mucArchiveQueryHandler);
+        queryHandlers.add(userArchiveQueryHandler);
+        this.queryHandlers = Collections.unmodifiableList(queryHandlers);
+    }
+
+    @Override
+    protected boolean verifyInnerElement(Stanza stanza) {
+        return verifyInnerElementWorker(stanza, Query.ELEMENT_NAME) && verifyInnerNamespace(stanza, namespace);
+    }
+
+    @Override
+    protected List<Stanza> handleSet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext,
+            SessionContext sessionContext, StanzaBroker broker) {
+        Query query;
+        try {
+            query = new Query(namespace, stanza);
+        } catch (XMLSemanticError xmlSemanticError) {
+            return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.NOT_ACCEPTABLE,
+                    stanza, StanzaErrorType.CANCEL, null, null, null));
+        }
+
+        return queryHandlers.stream().filter(handler -> handler.supports(query, serverRuntimeContext, sessionContext))
+                .map(handler -> handler.handle(query, serverRuntimeContext, sessionContext)).findFirst()
+                .orElseGet(() -> Collections.singletonList(ServerErrorResponses.getStanzaError(
+                        StanzaErrorCondition.NOT_ACCEPTABLE, stanza, StanzaErrorType.CANCEL, null, null, null)));
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java
new file mode 100644
index 0000000..3f9c638
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java
@@ -0,0 +1,108 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLFragment;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.datetime.DateTimeProfile;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+class MatchingArchivedMessageResult {
+
+    private static final String STANZA_ID = "stanza-id";
+
+    private final Entity initiatingEntity;
+
+    private final Entity archiveId;
+
+    private final Query query;
+
+    private final ArchivedMessage archivedMessage;
+
+    MatchingArchivedMessageResult(Entity initiatingEntity, Entity archiveId, Query query,
+            ArchivedMessage archivedMessage) {
+        this.initiatingEntity = requireNonNull(initiatingEntity);
+        this.archiveId = requireNonNull(archiveId);
+        this.query = requireNonNull(query);
+        this.archivedMessage = requireNonNull(archivedMessage);
+    }
+
+    Stanza toStanza() {
+        XMLElement result = createResult();
+        return new StanzaBuilder("message").addAttribute("to", initiatingEntity.getFullQualifiedName())
+                .addPreparedElement(result).build();
+    }
+
+    private XMLElement createResult() {
+        XMLElement forwarded = createForwarded();
+        List<Attribute> attributes = new ArrayList<>();
+        attributes.add(new Attribute("id", archivedMessage.id()));
+        query.getQueryId().map(queryId -> new Attribute("queryid", queryId)).ifPresent(attributes::add);
+        return new XMLElement(query.getNamespace(), "result", null, attributes, Collections.singletonList(forwarded));
+    }
+
+    private XMLElement createForwarded() {
+        Stanza archivedStanzaWithId = completeMessageStanzaWithId();
+
+        String stamp = DateTimeProfile.getInstance().getDateTimeInUTC(archivedMessage.dateTime());
+
+        List<XMLFragment> innerElements = new ArrayList<>();
+        innerElements.add(new XMLElement(NamespaceURIs.URN_XMPP_DELAY, "delay", null,
+                Collections.singletonList(new Attribute("stamp", stamp)), Collections.emptyList()));
+        innerElements.add(archivedStanzaWithId);
+        return new XMLElement(NamespaceURIs.XEP0297_STANZA_FORWARDING, "forwarded", null, Collections.emptyList(),
+                innerElements);
+    }
+
+    private Stanza completeMessageStanzaWithId() {
+        MessageStanza archivedMessageStanza = archivedMessage.stanza();
+
+        List<XMLElement> innerElements = new ArrayList<>();
+        archivedMessageStanza.getInnerElements().stream().filter(xmlElement -> !STANZA_ID.equals(xmlElement.getName()))
+                .filter(xmlElement -> !NamespaceURIs.XEP0359_STANZA_IDS.equals(xmlElement.getNamespaceURI()))
+                .forEach(innerElements::add);
+        List<Attribute> stanzaIdAttributes = new ArrayList<>();
+        stanzaIdAttributes.add(new Attribute("by", archiveId.getFullQualifiedName()));
+        stanzaIdAttributes.add(new Attribute("id", archivedMessage.id()));
+        innerElements.add(new XMLElement(NamespaceURIs.XEP0359_STANZA_IDS, STANZA_ID, null, stanzaIdAttributes,
+                Collections.emptyList()));
+
+        StanzaBuilder archivedMessageStanzaWithIdBuilder = StanzaBuilder.createClone(archivedMessageStanza, false,
+                Collections.emptyList());
+        innerElements.forEach(archivedMessageStanzaWithIdBuilder::addPreparedElement);
+        return archivedMessageStanzaWithIdBuilder.build();
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java
new file mode 100644
index 0000000..d02931a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java
@@ -0,0 +1,93 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.stanza.IQStanzaType;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MatchingArchivedMessageResults {
+
+    private final Entity initiatingEntity;
+
+    private final Entity archiveId;
+
+    private final Query query;
+
+    private final ArchivedMessages archivedMessages;
+
+    public MatchingArchivedMessageResults(Entity initiatingEntity, Entity archiveId, Query query,
+            ArchivedMessages archivedMessages) {
+        this.initiatingEntity = requireNonNull(initiatingEntity);
+        this.archiveId = requireNonNull(archiveId);
+        this.query = requireNonNull(query);
+        this.archivedMessages = requireNonNull(archivedMessages);
+    }
+
+    public List<Stanza> toStanzas() {
+        List<Stanza> stanzas = new ArrayList<>();
+        archivedMessages.list().stream().map(archivedMessage -> new MatchingArchivedMessageResult(initiatingEntity,
+                archiveId, query, archivedMessage)).map(MatchingArchivedMessageResult::toStanza).forEach(stanzas::add);
+        stanzas.add(buildResultIq());
+        return stanzas;
+    }
+
+    private Stanza buildResultIq() {
+        Set set = buildSet();
+        List<Attribute> finAttributes = new ArrayList<>();
+        if (archivedMessages.isComplete()) {
+            finAttributes.add(new Attribute("complete", "true"));
+        }
+        XMLElement fin = new XMLElement(query.getNamespace(), "fin", null, finAttributes,
+                Collections.singletonList(set.element()));
+        return StanzaBuilder.createDirectReply(query.iqStanza(), false, IQStanzaType.RESULT).addPreparedElement(fin)
+                .build();
+    }
+
+    private Set buildSet() {
+        if (archivedMessages.isEmpty()) {
+            return Set.builder().count(archivedMessages.totalNumberOfMessages().orElse(null)).build();
+        }
+
+        List<ArchivedMessage> messagesList = archivedMessages.list();
+
+        ArchivedMessage firstMessage = messagesList.get(0);
+        ArchivedMessage lastMessage = messagesList.get(messagesList.size() - 1);
+
+        return Set.builder().startFirst().index(archivedMessages.firstMessageIndex().orElse(null))
+                .value(firstMessage.id()).endFirst().last(lastMessage.id())
+                .count(archivedMessages.totalNumberOfMessages().orElse(null)).build();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java
new file mode 100644
index 0000000..a17ef65
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java
@@ -0,0 +1,88 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class Query {
+
+    public static final String ELEMENT_NAME = "query";
+
+    private final String namespace;
+
+    private final IQStanza iqStanza;
+
+    private final XMLElement element;
+
+    public Query(String namespace, IQStanza iqStanza) throws XMLSemanticError {
+        this.namespace = requireNonNull(namespace);
+        this.iqStanza = iqStanza;
+        this.element = iqStanza.getSingleInnerElementsNamed(ELEMENT_NAME);
+        if (!ELEMENT_NAME.equals(element.getName())) {
+            throw new IllegalArgumentException(
+                    "Query element must be named '" + ELEMENT_NAME + "' instead of '" + element.getName() + "'");
+        }
+        if (!namespace.equals(element.getNamespaceURI())) {
+            throw new IllegalArgumentException("Query element must be bound to namespace uri '" + namespace
+                    + "' instead of '" + element.getNamespaceURI() + "'");
+        }
+    }
+
+    public String getNamespace() {
+        return namespace;
+    }
+
+    public Optional<String> getQueryId() {
+        return ofNullable(element.getAttributeValue("queryid"));
+    }
+
+    public Optional<String> getNode() {
+        return ofNullable(element.getAttributeValue("node"));
+    }
+
+    public X getX() throws XMLSemanticError {
+        return ofNullable(element.getSingleInnerElementsNamed(X.ELEMENT_NAME))
+                .map(element1 -> new X(namespace, element1)).orElseGet(() -> X.empty(namespace));
+    }
+
+    public QuerySet getSet() throws XMLSemanticError {
+        XMLElement setElement = element.getSingleInnerElementsNamed(Set.ELEMENT_NAME);
+        if (setElement == null) {
+            return QuerySet.empty();
+        }
+        return new QuerySet(new Set(setElement));
+    }
+
+    public IQStanza iqStanza() {
+        return iqStanza;
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java
new file mode 100644
index 0000000..0ce937c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java
@@ -0,0 +1,37 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import java.util.List;
+
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.Stanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface QueryHandler {
+
+    boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext);
+
+    List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java
new file mode 100644
index 0000000..968f3be
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java
@@ -0,0 +1,78 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class QuerySet implements MessagePageRequest {
+
+    private final Long max;
+
+    private final String after;
+
+    private final String before;
+
+    public QuerySet(Set set) throws XMLSemanticError {
+        this.max = set.getMax().orElse(null);
+        this.after = set.getAfter().orElse(null);
+        this.before = set.getBefore().orElse(null);
+    }
+
+    public static QuerySet empty() throws XMLSemanticError {
+        return new QuerySet(Set.empty());
+    }
+
+    @Override
+    public Optional<Long> pageSize() {
+        return ofNullable(max);
+    }
+
+    @Override
+    public Optional<String> firstMessageId() {
+        return ofNullable(after);
+    }
+
+    @Override
+    public Optional<String> lastMessageId() {
+        return ofNullable(before);
+    }
+
+    /**
+     * <a href="https://xmpp.org/extensions/xep-0059.html#last">Requesting the Last
+     * Page in a Result Set</a>
+     *
+     * The requesting entity MAY ask for the last page in a result set by including
+     * in its request an empty <before/> element, and the maximum number of items to
+     * return.
+     */
+    public boolean lastPage() {
+        return max != null && after == null && ofNullable(before).filter(StringUtils::isBlank).isPresent();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java
new file mode 100644
index 0000000..86faa91
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java
@@ -0,0 +1,83 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Optional.ofNullable;
+
+import java.time.ZonedDateTime;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.datetime.DateTimeProfile;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.stanza.dataforms.DataFormParser;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class X implements DateTimeFilter {
+
+    public static final String ELEMENT_NAME = "x";
+
+    private final Map<String, Object> fields;
+
+    public X(String namespace, XMLElement element) {
+        if (!ELEMENT_NAME.equals(element.getName())) {
+            throw new IllegalArgumentException(
+                    "Query element must be named '" + ELEMENT_NAME + "' instead of '" + element.getName() + "'");
+        }
+        if (!NamespaceURIs.JABBER_X_DATA.equals(element.getNamespaceURI())) {
+            throw new IllegalArgumentException("Query element must be bound to namespace uri '" + namespace
+                    + "' instead of '" + element.getNamespaceURI() + "'");
+        }
+        fields = new DataFormParser(element).extractFieldValues();
+    }
+
+    public static X empty(String namespace) {
+        return new X(namespace, new XMLElement(NamespaceURIs.JABBER_X_DATA, ELEMENT_NAME, null, Collections.emptyList(),
+                Collections.emptyList()));
+    }
+
+    private Optional<String> getStringValue(String fieldName) {
+        return ofNullable(fields.get(fieldName)).map(String.class::cast);
+    }
+
+    private Optional<ZonedDateTime> getZonedDateTime(String fieldName) {
+        return getStringValue(fieldName).map(stringDate -> DateTimeProfile.getInstance().fromZonedDateTime(stringDate));
+    }
+
+    public Optional<Entity> getWith() {
+        return getStringValue("with").map(EntityImpl::parseUnchecked);
+    }
+
+    public Optional<ZonedDateTime> start() {
+        return getZonedDateTime("start");
+    }
+
+    public Optional<ZonedDateTime> end() {
+        return getZonedDateTime("end");
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java
new file mode 100644
index 0000000..3a20f30
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java
@@ -0,0 +1,34 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface ArchivedMessage extends Message {
+
+    /**
+     * @return The id of the message in its archive as defined in <a
+     *         href="https://xmpp.org/extensions/xep-0313.html#archives_id">Communicating
+     *         the archive ID</a>
+     */
+    String id();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java
new file mode 100644
index 0000000..a8e3cb1
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java
@@ -0,0 +1,60 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface ArchivedMessages {
+
+    List<ArchivedMessage> list();
+
+    boolean isEmpty();
+
+    /**
+     * When the results returned by the server are complete (that is: when they have
+     * not been limited by the maximum size of the result page (either as specified
+     * or enforced by the server)), the server MUST include a 'complete' attribute
+     * on the <fin> element, with a value of 'true'; this informs the client that it
+     * doesn't need to perform further paging to retreive the requested data. If it
+     * is not the last page of the result set, the server MUST either omit the
+     * 'complete' attribute, or give it a value of 'false'.
+     */
+    boolean isComplete();
+
+    /**
+     * This integer specifies the position within the full set (which MAY be
+     * approximate) of the first message in the page. If that message is the first
+     * in the full set, then the index SHOULD be '0'. If the last message in the
+     * page is the last message in the full set, then the value SHOULD be the
+     * specified count minus the number of messages in the last page.
+     */
+    Optional<Long> firstMessageIndex();
+
+    /**
+     * @return The total number of messages that could be retrieved by paging
+     *         through the pages.
+     */
+    Optional<Long> totalNumberOfMessages();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java
new file mode 100644
index 0000000..0a033d8
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java
@@ -0,0 +1,48 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/**
+ * <a href="https://xmpp.org/extensions/xep-0313.html#filter-time">Filtering by
+ * time received</a>
+ * 
+ * @author Réda Housni Alaoui
+ */
+public interface DateTimeFilter {
+
+    /**
+     * The 'start' field is used to filter out messages before a certain date/time.
+     * If specified, a server MUST only return messages whose timestamp is equal to
+     * or later than the given timestamp.
+     */
+    Optional<ZonedDateTime> start();
+
+    /**
+     * The 'end' field is used to exclude from the results messages
+     * after a certain point in time. If specified, a server MUST only return
+     * messages whose timestamp is equal to or earlier than the timestamp given in
+     * the 'end' field.
+     */
+    Optional<ZonedDateTime> end();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java
new file mode 100644
index 0000000..f274c11
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java
@@ -0,0 +1,56 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface EntityFilter {
+
+    enum Type {
+        /**
+         * The message 'to' or 'from' must match the provided entity
+         */
+        TO_OR_FROM,
+        /**
+         * The message 'to' and 'from' must match the provided entity
+         */
+        TO_AND_FROM;
+    }
+
+    /**
+     * @return The entity to filter by
+     */
+    Entity entity();
+
+    /**
+     * @return The type of filtering
+     */
+    Type type();
+
+    /**
+     * @return True to ignore the filtering entity, the message 'to' and the message
+     *         'from' <b>resource part</b>.
+     */
+    boolean ignoreResource();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java
new file mode 100644
index 0000000..0ef98e5
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java
@@ -0,0 +1,35 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface Message {
+
+    MessageStanza stanza();
+
+    ZonedDateTime dateTime();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java
new file mode 100644
index 0000000..99d2d06
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java
@@ -0,0 +1,43 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+/**
+ * <a href=
+ * "https://xmpp.org/extensions/xep-0313.html#business-storeret">Storage and
+ * Retrieval Rules</a>
+ * 
+ * @author Réda Housni Alaoui
+ */
+public interface MessageArchive {
+
+    /**
+     * At a minimum, the server MUST store the <body> elements of a stanza. It is
+     * suggested that other elements that are used in a given deployment to
+     * supplement conversations (e.g. XHTML-IM payloads) are also stored. Other
+     * elements MAY be stored.
+     */
+    void archive(Message message);
+
+    ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest);
+
+    ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java
new file mode 100644
index 0000000..e4df43b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java
@@ -0,0 +1,34 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+import org.apache.vysper.storage.StorageProvider;
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessageArchives extends StorageProvider {
+
+    Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java
new file mode 100644
index 0000000..6110f2b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java
@@ -0,0 +1,33 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessageFilter {
+
+    Optional<EntityFilter> entityFilter();
+
+    Optional<DateTimeFilter> dateTimeFilter();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java
new file mode 100644
index 0000000..6f04adf
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java
@@ -0,0 +1,35 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessagePageRequest {
+
+    Optional<Long> pageSize();
+
+    Optional<String> firstMessageId();
+
+    Optional<String> lastMessageId();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java
new file mode 100644
index 0000000..80e271a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java
@@ -0,0 +1,82 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleArchivedMessage implements ArchivedMessage {
+
+    private final String id;
+
+    private final ZonedDateTime dateTime;
+
+    private final MessageStanza stanza;
+
+    public SimpleArchivedMessage(String id, Message message) {
+        this(id, message.dateTime(), message.stanza());
+    }
+
+    public SimpleArchivedMessage(String id, ZonedDateTime dateTime, MessageStanza stanza) {
+        this.id = requireNonNull(id);
+        this.dateTime = requireNonNull(dateTime);
+        this.stanza = requireNonNull(stanza);
+    }
+
+    @Override
+    public MessageStanza stanza() {
+        return stanza;
+    }
+
+    @Override
+    public ZonedDateTime dateTime() {
+        return dateTime;
+    }
+
+    @Override
+    public String id() {
+        return id;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        SimpleArchivedMessage that = (SimpleArchivedMessage) o;
+
+        return id.equals(that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java
new file mode 100644
index 0000000..342c346
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java
@@ -0,0 +1,82 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleArchivedMessages implements ArchivedMessages {
+
+    private final List<ArchivedMessage> list;
+
+    private final Long firstMessageIndex;
+
+    private final Long totalNumberOfMessages;
+
+    public SimpleArchivedMessages(List<ArchivedMessage> list) {
+        this(list, null, null);
+    }
+
+    public SimpleArchivedMessages(List<ArchivedMessage> list, Long firstMessageIndex) {
+        this(list, firstMessageIndex, null);
+    }
+
+    public SimpleArchivedMessages(List<ArchivedMessage> list, Long firstMessageIndex, Long totalNumberOfMessages) {
+        this.list = new ArrayList<>(list);
+        this.firstMessageIndex = firstMessageIndex;
+        this.totalNumberOfMessages = totalNumberOfMessages;
+    }
+
+    public static SimpleArchivedMessages empty() {
+        return new SimpleArchivedMessages(Collections.emptyList());
+    }
+
+    @Override
+    public List<ArchivedMessage> list() {
+        return new ArrayList<>(list);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return list.isEmpty();
+    }
+
+    @Override
+    public boolean isComplete() {
+        return list.size() == totalNumberOfMessages;
+    }
+
+    @Override
+    public Optional<Long> firstMessageIndex() {
+        return ofNullable(firstMessageIndex);
+    }
+
+    @Override
+    public Optional<Long> totalNumberOfMessages() {
+        return ofNullable(totalNumberOfMessages);
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java
new file mode 100644
index 0000000..1d28b37
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java
@@ -0,0 +1,93 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.ArchiveQuery;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.MatchingArchivedMessageResults;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveQueryHandler implements QueryHandler {
+
+    @Override
+    public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+        return true;
+    }
+
+    @Override
+    public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+        try {
+            return doHandle(query, serverRuntimeContext, sessionContext);
+        } catch (XMLSemanticError xmlSemanticError) {
+            Stanza internalServerError = ServerErrorResponses.getStanzaError(StanzaErrorCondition.INTERNAL_SERVER_ERROR,
+                    query.iqStanza(), StanzaErrorType.CANCEL, null, null, null);
+            return Collections.singletonList(internalServerError);
+        }
+    }
+
+    private List<Stanza> doHandle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext)
+            throws XMLSemanticError {
+        Entity initiatingEntity = sessionContext.getInitiatingEntity();
+        Entity archiveId = ofNullable(query.iqStanza().getTo()).orElse(initiatingEntity).getBareJID();
+
+        if (!sessionContext.getInitiatingEntity().getBareJID().equals(archiveId)) {
+            // The initiating user is trying to read the archive of another user
+            return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.FORBIDDEN,
+                    query.iqStanza(), StanzaErrorType.CANCEL,
+                    "Entity " + initiatingEntity + " is not allowed to query " + archiveId + " message archives.", null,
+                    null));
+        }
+
+        MessageArchives archives = requireNonNull(
+                (MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+                "Could not find an instance of " + MessageArchives.class);
+
+        Optional<MessageArchive> archive = archives.retrieveUserMessageArchive(archiveId);
+        if (!archive.isPresent()) {
+            return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.ITEM_NOT_FOUND,
+                    query.iqStanza(), StanzaErrorType.CANCEL, "No user message archive found for entity " + archiveId,
+                    null, null));
+        }
+
+        ArchivedMessages archivedMessages = new ArchiveQuery(archive.get(), archiveId, query).execute();
+        return new MatchingArchivedMessageResults(initiatingEntity, archiveId, query, archivedMessages).toStanzas();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java
new file mode 100644
index 0000000..97998ea
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java
@@ -0,0 +1,93 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Optional;
+
+import org.apache.vysper.event.EventListener;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.core.base.handler.XMPPCoreStanzaHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SimpleMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanzaType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <a href=
+ * "https://xmpp.org/extensions/xep-0313.html#business-storeret-user-archives">User
+ * Archives</a>
+ * 
+ * @author Réda Housni Alaoui
+ */
+public class UserMessageListener implements EventListener<AcceptedMessageEvent> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(UserMessageListener.class);
+
+    @Override
+    public void onEvent(AcceptedMessageEvent acceptedMessage) {
+        archive(acceptedMessage);
+    }
+
+    private void archive(AcceptedMessageEvent acceptedMessage) {
+        MessageStanza messageStanza = acceptedMessage.messageStanza();
+        MessageStanzaType messageStanzaType = messageStanza.getMessageType();
+        if (messageStanzaType != MessageStanzaType.NORMAL && messageStanzaType != MessageStanzaType.CHAT) {
+            // A server SHOULD include in a user archive all of the messages a user sends
+            // or receives of type 'normal' or 'chat' that contain a <body> element.
+            LOG.debug("Message {} is neither of type 'normal' or 'chat'. It will not be archived.", messageStanza);
+            return;
+        }
+
+        Entity archiveJID;
+        if (acceptedMessage.isOutbound()) {
+            // We will store the message in the sender archive
+            SessionContext sessionContext = acceptedMessage.sessionContext();
+            archiveJID = XMPPCoreStanzaHandler.extractSenderJID(messageStanza, sessionContext);
+        } else {
+            // We will store the message in the receiver archive
+            archiveJID = requireNonNull(messageStanza.getTo(), "No 'to' found in " + messageStanza);
+        }
+
+        // Servers that expose archive messages of sent/received messages on behalf of
+        // local users MUST expose these archives to the user on the user's bare JID.
+        archiveJID = archiveJID.getBareJID();
+
+        ServerRuntimeContext serverRuntimeContext = acceptedMessage.serverRuntimeContext();
+        MessageArchives archives = requireNonNull(
+                (MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+                "Could not find an instance of " + MessageArchives.class);
+
+        Optional<MessageArchive> userArchive = archives.retrieveUserMessageArchive(archiveJID);
+        if (!userArchive.isPresent()) {
+            LOG.debug("No archive returned for user with bare JID '{}'", archiveJID);
+            return;
+        }
+
+        userArchive.get().archive(new SimpleMessage(messageStanza));
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java
new file mode 100644
index 0000000..035ba88
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java
@@ -0,0 +1,203 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.apache.vysper.mina.C2SEndpoint;
+import org.apache.vysper.mina.TCPEndpoint;
+import org.apache.vysper.storage.StorageProviderRegistry;
+import org.apache.vysper.storage.inmemory.MemoryStorageProviderRegistry;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.authentication.AccountManagement;
+import org.apache.vysper.xmpp.cryptography.NonCheckingX509TrustManagerFactory;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory.InMemoryMessageArchives;
+import org.apache.vysper.xmpp.server.XMPPServer;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.StanzaCollector;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.debugger.ConsoleDebugger;
+import org.jivesoftware.smack.filter.StanzaIdFilter;
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.junit.After;
+import org.junit.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public abstract class IntegrationTest {
+
+    private final Logger logger = LoggerFactory.getLogger(IntegrationTest.class);
+
+    private static final String TLS_CERTIFICATE_PATH = "src/test/resources/bogus_mina_tls.cert";
+
+    private static final String TLS_CERTIFICATE_PASSWORD = "boguspw";
+
+    private static final String SERVER_DOMAIN = "vysper.org";
+
+    private static final String PASSWORD = "password";
+
+    private static final int DEFAULT_SERVER_PORT = 25222;
+
+    private static final String ALICE_USERNAME = "test1@" + SERVER_DOMAIN;
+
+    private static final String CAROL_USERNAME = "test2@" + SERVER_DOMAIN;
+
+    private XMPPTCPConnection aliceClient;
+
+    private XMPPTCPConnection carolClient;
+
+    private XMPPServer server;
+
+    @Before
+    public void setUp() throws Exception {
+        SmackConfiguration.setDefaultReplyTimeout(5000);
+
+        int port = findFreePort();
+
+        startServer(port);
+
+        aliceClient = connectClient(port, ALICE_USERNAME);
+        carolClient = connectClient(port, CAROL_USERNAME);
+    }
+
+    protected XMPPConnection alice() {
+        return aliceClient;
+    }
+
+    protected XMPPConnection carol() {
+        return carolClient;
+    }
+
+    protected Stanza sendSync(XMPPConnection client, Stanza request)
+            throws SmackException.NotConnectedException, InterruptedException {
+        StanzaCollector collector = client.createStanzaCollector(new StanzaIdFilter(request.getStanzaId()));
+
+        client.sendStanza(request);
+
+        return collector.nextResult(5000);
+    }
+
+    private void startServer(int port) throws Exception {
+        StorageProviderRegistry providerRegistry = new MemoryStorageProviderRegistry();
+
+        AccountManagement accountManagement = (AccountManagement) providerRegistry.retrieve(AccountManagement.class);
+        accountManagement.addUser(EntityImpl.parseUnchecked(ALICE_USERNAME), PASSWORD);
+        accountManagement.addUser(EntityImpl.parseUnchecked(CAROL_USERNAME), PASSWORD);
+
+        server = new XMPPServer(SERVER_DOMAIN);
+
+        TCPEndpoint endpoint = new C2SEndpoint();
+        endpoint.setPort(port);
+        server.addEndpoint(endpoint);
+        server.setStorageProviderRegistry(providerRegistry);
+
+        server.setTLSCertificateInfo(new File(TLS_CERTIFICATE_PATH), TLS_CERTIFICATE_PASSWORD);
+
+        server.start();
+
+        providerRegistry.add(new InMemoryMessageArchives());
+        server.addModule(new MAMModule());
+
+        Thread.sleep(200);
+    }
+
+    private XMPPTCPConnection connectClient(int port, String username) throws Exception {
+        XMPPTCPConnectionConfiguration connectionConfiguration = XMPPTCPConnectionConfiguration.builder()
+                .setHost("localhost").setPort(port).setXmppDomain(SERVER_DOMAIN)
+                .setHostnameVerifier((hostname, session) -> true).setCompressionEnabled(false)
+                .setSecurityMode(ConnectionConfiguration.SecurityMode.required)
+                .addEnabledSaslMechanism(SASLMechanism.PLAIN).setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE)
+                .setKeystorePath(TLS_CERTIFICATE_PATH)
+                .setCustomX509TrustManager(NonCheckingX509TrustManagerFactory.X509).build();
+
+        XMPPTCPConnection client = new XMPPTCPConnection(connectionConfiguration);
+
+        client.connect();
+
+        client.login(username, PASSWORD);
+        return client;
+    }
+
+    private int findFreePort() {
+        ServerSocket ss = null;
+
+        // try using a predefined default port
+        // makes netstat -a debugging easier
+        try {
+            ss = new ServerSocket(DEFAULT_SERVER_PORT);
+            ss.setReuseAddress(true);
+
+            // succeeded, return the default port
+            logger.info("Test is using the default test port {}", DEFAULT_SERVER_PORT);
+            return DEFAULT_SERVER_PORT;
+        } catch (IOException e) {
+            try {
+                ss = new ServerSocket(0);
+                ss.setReuseAddress(true);
+                int port = ss.getLocalPort();
+                logger.info("Failed to use default test port ({}), using {} instead", DEFAULT_SERVER_PORT, port);
+                return port;
+            } catch (IOException ee) {
+                // we could not even open a random port so
+                // the test will probably fail, anyways
+                // return the default port
+                return DEFAULT_SERVER_PORT;
+            }
+        } finally {
+            if (ss != null) {
+                try {
+                    ss.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    @After
+    public void tearDown() {
+        try {
+            aliceClient.disconnect();
+        } catch (Exception ignored) {
+
+        }
+
+        try {
+            carolClient.disconnect();
+        } catch (Exception ignored) {
+
+        }
+
+        try {
+            server.stop();
+        } catch (Exception ignored) {
+        }
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java
new file mode 100644
index 0000000..115e23a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java
@@ -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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.servicediscovery.management.Feature;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoElement;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequest;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServerInfoRequestListener;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServiceDiscoveryRequestException;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.junit.Test;
+
+import junit.framework.Assert;
+
+/**
+ * @author The Apache MINA Project (dev@mina.apache.org)
+ */
+public class MAMModuleTest {
+
+    private MAMModule tested = new MAMModule();
+
+    @Test
+    public void nameMustBeProvided() {
+        Assert.assertNotNull(tested.getName());
+    }
+
+    @Test
+    public void versionMustBeProvided() {
+        Assert.assertNotNull(tested.getVersion());
+    }
+
+    @Test
+    public void getServerInfosFor() throws ServiceDiscoveryRequestException {
+        List<ServerInfoRequestListener> serverInfoRequestListeners = new ArrayList<>();
+
+        tested.addServerInfoRequestListeners(serverInfoRequestListeners);
+
+        Assert.assertEquals(1, serverInfoRequestListeners.size());
+
+        List<InfoElement> infoElements = serverInfoRequestListeners.get(0)
+                .getServerInfosFor(new InfoRequest(null, null, null, null));
+
+        Assert.assertEquals(4, infoElements.size());
+        Assert.assertTrue(infoElements.get(0) instanceof Feature);
+        Assert.assertTrue(infoElements.get(1) instanceof Feature);
+        Assert.assertTrue(infoElements.get(2) instanceof Feature);
+        Assert.assertTrue(infoElements.get(3) instanceof Feature);
+        Assert.assertEquals("urn:xmpp:mam:1", ((Feature) infoElements.get(0)).getVar());
+        Assert.assertEquals("urn:xmpp:mam:2", ((Feature) infoElements.get(1)).getVar());
+        Assert.assertEquals(NamespaceURIs.XEP0359_STANZA_IDS, ((Feature) infoElements.get(2)).getVar());
+        Assert.assertEquals(NamespaceURIs.JABBER_X_DATA, ((Feature) infoElements.get(3)).getVar());
+    }
+
+    @Test
+    public void getServerInfosForWithNode() throws ServiceDiscoveryRequestException {
+        List<ServerInfoRequestListener> serverInfoRequestListeners = new ArrayList<>();
+        tested.addServerInfoRequestListeners(serverInfoRequestListeners);
+        Assert.assertEquals(1, serverInfoRequestListeners.size());
+
+        Assert.assertNull(
+                serverInfoRequestListeners.get(0).getServerInfosFor(new InfoRequest(null, null, "node", null)));
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java
new file mode 100644
index 0000000..a78326a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java
@@ -0,0 +1,158 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.vysper.event.EventBus;
+import org.apache.vysper.storage.StorageProvider;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.authentication.UserAuthentication;
+import org.apache.vysper.xmpp.delivery.StanzaRelay;
+import org.apache.vysper.xmpp.modules.Module;
+import org.apache.vysper.xmpp.modules.ServerRuntimeContextService;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.protocol.StanzaHandler;
+import org.apache.vysper.xmpp.protocol.StanzaProcessor;
+import org.apache.vysper.xmpp.server.ServerFeatures;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.components.Component;
+import org.apache.vysper.xmpp.server.components.ComponentStanzaProcessor;
+import org.apache.vysper.xmpp.server.s2s.XMPPServerConnectorRegistry;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.state.presence.LatestPresenceCache;
+import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ServerRuntimeContextMock implements ServerRuntimeContext {
+
+    private MessageArchivesMock userMessageArchives;
+
+    public MessageArchivesMock givenUserMessageArchives() {
+        userMessageArchives = new MessageArchivesMock();
+        return userMessageArchives;
+    }
+
+    @Override
+    public StanzaHandler getHandler(Stanza stanza) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getNextSessionId() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Entity getServerEntity() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getDefaultXMLLang() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ServerFeatures getServerFeatures() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SSLContext getSslContext() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public UserAuthentication getUserAuthentication() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResourceRegistry getResourceRegistry() {
+        return mock(ResourceRegistry.class);
+    }
+
+    @Override
+    public LatestPresenceCache getPresenceCache() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void registerServerRuntimeContextService(ServerRuntimeContextService service) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ServerRuntimeContextService getServerRuntimeContextService(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends StorageProvider> T getStorageProvider(Class<T> clazz) {
+        if (MessageArchives.class.equals(clazz)) {
+            return (T) userMessageArchives;
+        }
+        return null;
+    }
+
+    @Override
+    public void registerComponent(Component component) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasComponentStanzaProcessor(Entity entity) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public XMPPServerConnectorRegistry getServerConnectorRegistry() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<Module> getModules() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> T getModule(Class<T> clazz) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addModule(Module module) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public EventBus getEventBus() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java
new file mode 100644
index 0000000..5b69042
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java
@@ -0,0 +1,145 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.SessionState;
+import org.apache.vysper.xmpp.state.resourcebinding.BindException;
+import org.apache.vysper.xmpp.writer.StanzaWriter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SessionContextMock implements SessionContext {
+
+    private ServerRuntimeContext serverRuntimeContext;
+
+    private Entity initiatingEntity;
+
+    public void givenServerRuntimeContext(ServerRuntimeContext serverRuntimeContext) {
+        this.serverRuntimeContext = serverRuntimeContext;
+    }
+
+    public void givenInitiatingEntity(Entity initiatingEntity) {
+        this.initiatingEntity = initiatingEntity;
+    }
+
+    @Override
+    public ServerRuntimeContext getServerRuntimeContext() {
+        return serverRuntimeContext;
+    }
+
+    @Override
+    public boolean isRemotelyInitiatedSession() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Entity getInitiatingEntity() {
+        return initiatingEntity;
+    }
+
+    @Override
+    public void setInitiatingEntity(Entity entity) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isServerToServer() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setServerToServer() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setClientToServer() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SessionState getState() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSessionId() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getXMLLang() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setXMLLang(String languageCode) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public StanzaWriter getResponseWriter() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void endSession(SessionTerminationCause terminationCause) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Entity getServerJID() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void switchToTLS(boolean delayed, boolean clientTls) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setIsReopeningXMLStream() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String bindResource() throws BindException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String nextSequenceValue() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object putAttribute(String key, Object value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getAttribute(String key) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java
new file mode 100644
index 0000000..bbf9369
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java
@@ -0,0 +1,37 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import org.apache.vysper.xml.fragment.Renderer;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.junit.Assert;
+
+public class StanzaAssert {
+
+    public static void assertEquals(Stanza expected, Stanza actual) {
+        try {
+            Assert.assertEquals(expected, actual);
+        } catch(Throwable e) {
+            // print something useful
+            Assert.assertEquals(new Renderer(expected).getComplete(), new Renderer(actual).getComplete());
+        }
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java
new file mode 100644
index 0000000..bd0eed0
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java
@@ -0,0 +1,149 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.chat2.Chat;
+import org.jivesoftware.smack.chat2.ChatManager;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.forward.packet.Forwarded;
+import org.jivesoftware.smackx.mam.MamManager;
+import org.jivesoftware.smackx.sid.element.StableAndUniqueIdElement;
+import org.jivesoftware.smackx.sid.element.StanzaIdElement;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveTest extends IntegrationTest {
+
+    @Test
+    public void queryEmptyArchive()
+            throws XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException,
+            SmackException.NotLoggedInException, SmackException.NoResponseException {
+        MamManager.MamQueryArgs mamQueryArgs = MamManager.MamQueryArgs.builder().build();
+        MamManager.MamQuery mamQuery = MamManager.getInstanceFor(alice()).queryArchive(mamQueryArgs);
+
+        assertEquals(0, mamQuery.getMessageCount());
+    }
+
+    @Test
+    public void sendMessageAndQuerySenderAndReceiverArchive()
+            throws SmackException.NotConnectedException, InterruptedException, SmackException.NotLoggedInException,
+            XMPPException.XMPPErrorException, SmackException.NoResponseException {
+        Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+        chat.send("Hello carol");
+
+        MamManager.MamQueryArgs fullQuery = MamManager.MamQueryArgs.builder().build();
+
+        MamManager.MamQuery aliceArchive = MamManager.getInstanceFor(alice()).queryArchive(fullQuery);
+
+        assertEquals(1, aliceArchive.getMessageCount());
+        Message toCarolMessage = aliceArchive.getMessages().get(0);
+        assertEquals("Hello carol", toCarolMessage.getBody());
+        assertEquals(alice().getUser(), toCarolMessage.getFrom());
+        assertEquals(carol().getUser().asEntityBareJidOrThrow(), toCarolMessage.getTo().asEntityBareJidOrThrow());
+
+        MamManager.MamQuery carolArchive = MamManager.getInstanceFor(carol()).queryArchive(fullQuery);
+
+        assertEquals(1, carolArchive.getMessageCount());
+
+        Message fromAliceMessage = carolArchive.getMessages().get(0);
+        assertEquals("Hello carol", fromAliceMessage.getBody());
+        assertEquals(alice().getUser(), fromAliceMessage.getFrom());
+        assertEquals(carol().getUser().asEntityBareJidOrThrow(), fromAliceMessage.getTo().asEntityBareJidOrThrow());
+    }
+
+    @Test
+    public void checkSorting() throws SmackException.NotConnectedException, InterruptedException,
+            SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+        Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+        for (int index = 1; index <= 10; index++) {
+            chat.send("Hello " + index);
+        }
+
+        MamManager.MamQueryArgs fullQuery = MamManager.MamQueryArgs.builder().build();
+        MamManager.MamQuery aliceArchive = MamManager.getInstanceFor(alice()).queryArchive(fullQuery);
+
+        assertEquals(10, aliceArchive.getMessageCount());
+        assertTrue(aliceArchive.isComplete());
+
+        for (int index = 1; index <= 10; index++) {
+            assertEquals("Hello " + index, aliceArchive.getMessages().get(index - 1).getBody());
+        }
+    }
+
+    @Test
+    public void paginate() throws SmackException.NotConnectedException, InterruptedException,
+            SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+        Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+        for (int index = 1; index <= 10; index++) {
+            chat.send("Hello " + index);
+        }
+
+        MamManager mamManager = MamManager.getInstanceFor(alice());
+
+        MamManager.MamQueryArgs firstHalfPageable = MamManager.MamQueryArgs.builder().setResultPageSizeTo(5).build();
+
+        MamManager.MamQuery page = mamManager.queryArchive(firstHalfPageable);
+        assertFalse(page.isComplete());
+        assertEquals(5, page.getMessageCount());
+        for (int index = 1; index <= 5; index++) {
+            assertEquals("Hello " + index, page.getMessages().get(index - 1).getBody());
+        }
+
+        page.pageNext(5);
+        assertFalse(page.isComplete());
+        assertEquals(5, page.getMessageCount());
+        for (int index = 6; index <= 10; index++) {
+            assertEquals("Hello " + index, page.getMessages().get(index - 6).getBody());
+        }
+
+    }
+
+    @Test
+    public void lastPage() throws SmackException.NotConnectedException, InterruptedException,
+            SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+        Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+        for (int index = 1; index <= 10; index++) {
+            chat.send("Hello " + index);
+        }
+
+        MamManager mamManager = MamManager.getInstanceFor(alice());
+        MamManager.MamQuery fullQuery = mamManager.queryArchive(MamManager.MamQueryArgs.builder().build());
+
+        List<String> allMessageIds = fullQuery.getPage().getForwarded().stream().map(Forwarded::getForwardedStanza)
+                .map(stanza -> stanza.getExtension("stanza-id", NamespaceURIs.XEP0359_STANZA_IDS))
+                .map(StanzaIdElement.class::cast).map(StableAndUniqueIdElement::getId).collect(Collectors.toList());
+
+        String lastMessageUid = mamManager.getMessageUidOfLatestMessage();
+        assertEquals(allMessageIds.get(allMessageIds.size() - 1), lastMessageUid);
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java
new file mode 100644
index 0000000..9f80418
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java
@@ -0,0 +1,99 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleEntityFilter;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryEntityFilterTest {
+
+    private static final Entity ROMEO_SIDEWALK = EntityImpl.parseUnchecked("romeo@foo.com/sidewalk");
+
+    private static final Entity ROMEO_CAR = EntityImpl.parseUnchecked("romeo@foo.com/car");
+
+    private static final Entity JULIET_BALCONY = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+
+    private static final Entity JULIET_TRAIN = EntityImpl.parseUnchecked("juliet@foo.com/train");
+
+    private ArchivedMessage romeoSidewalkToJulietBalcony;
+
+    private ArchivedMessage romeoSidewalkToRomeoCar;
+
+    @Before
+    public void before() {
+        romeoSidewalkToJulietBalcony = mock(ArchivedMessage.class);
+        MessageStanza romeoSidewalkToJulietBalconyStanza = mock(MessageStanza.class);
+        when(romeoSidewalkToJulietBalconyStanza.getFrom()).thenReturn(ROMEO_SIDEWALK);
+        when(romeoSidewalkToJulietBalconyStanza.getTo()).thenReturn(JULIET_BALCONY);
+        when(romeoSidewalkToJulietBalcony.stanza()).thenReturn(romeoSidewalkToJulietBalconyStanza);
+
+        romeoSidewalkToRomeoCar = mock(ArchivedMessage.class);
+        MessageStanza romeoSidewalkToRomeoCarStanza = mock(MessageStanza.class);
+        when(romeoSidewalkToRomeoCarStanza.getFrom()).thenReturn(ROMEO_SIDEWALK);
+        when(romeoSidewalkToRomeoCarStanza.getTo()).thenReturn(ROMEO_CAR);
+        when(romeoSidewalkToRomeoCar.stanza()).thenReturn(romeoSidewalkToRomeoCarStanza);
+    }
+
+    @Test
+    public void toAndFromIgnoringResource() {
+        EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_AND_FROM, true);
+        InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+        assertTrue(tested.test(romeoSidewalkToRomeoCar));
+        assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+    }
+
+    @Test
+    public void toAndFromNotIgnoringResource() {
+        EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_AND_FROM, false);
+        InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+        assertFalse(tested.test(romeoSidewalkToRomeoCar));
+        assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+    }
+
+    @Test
+    public void toOrFromIgnoringResource() {
+        EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_OR_FROM, true);
+        InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+        assertTrue(tested.test(romeoSidewalkToRomeoCar));
+        assertTrue(tested.test(romeoSidewalkToJulietBalcony));
+    }
+
+    @Test
+    public void toOrFromNotIgnoringResource() {
+        EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_OR_FROM, false);
+        InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+        assertTrue(tested.test(romeoSidewalkToRomeoCar));
+        assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+    }
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java
new file mode 100644
index 0000000..cb3ec9e
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java
@@ -0,0 +1,160 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleMessagePageRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryPageLimitedArchivedMessagesTest {
+
+    private static final String MESSAGE_1_ID = "message-1";
+
+    private static final String MESSAGE_2_ID = "message-2";
+
+    private static final String MESSAGE_3_ID = "message-3";
+
+    private ArchivedMessage message1;
+
+    private ArchivedMessage message2;
+
+    private ArchivedMessage message3;
+
+    private List<ArchivedMessage> messages;
+
+    @Before
+    public void before() {
+        message1 = mock(ArchivedMessage.class);
+        when(message1.id()).thenReturn(MESSAGE_1_ID);
+        message2 = mock(ArchivedMessage.class);
+        when(message2.id()).thenReturn(MESSAGE_2_ID);
+        message3 = mock(ArchivedMessage.class);
+        when(message3.id()).thenReturn(MESSAGE_3_ID);
+
+        messages = new ArrayList<>();
+        messages.add(message1);
+        messages.add(message2);
+        messages.add(message3);
+    }
+
+    @Test
+    public void withoutLimit() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(null, null, null);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+        assertFalse(tested.isEmpty());
+        assertTrue(tested.isComplete());
+        assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+        List<ArchivedMessage> list = tested.list();
+        assertEquals(3, list.size());
+        assertEquals(message1, list.get(0));
+        assertEquals(message2, list.get(1));
+        assertEquals(message3, list.get(2));
+    }
+
+    @Test
+    public void withoutFirstOrLastMessageId() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, null, null);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+        assertFalse(tested.isEmpty());
+        assertFalse(tested.isComplete());
+        assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+        List<ArchivedMessage> list = tested.list();
+        assertEquals(2, list.size());
+        assertEquals(message1, list.get(0));
+        assertEquals(message2, list.get(1));
+    }
+
+    @Test
+    public void withFirstMessageId() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_2_ID, null);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+        assertFalse(tested.isEmpty());
+        assertFalse(tested.isComplete());
+        assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertEquals(2, (long) tested.firstMessageIndex().orElse(-1L));
+        List<ArchivedMessage> list = tested.list();
+        assertEquals(1, list.size());
+        assertEquals(message3, list.get(0));
+    }
+
+    @Test
+    public void withLastMessageId() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, null, MESSAGE_2_ID);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+        assertFalse(tested.isEmpty());
+        assertFalse(tested.isComplete());
+        assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+        List<ArchivedMessage> list = tested.list();
+        assertEquals(1, list.size());
+        assertEquals(message1, list.get(0));
+    }
+
+    @Test
+    public void withFirstAndLastMessageId() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_1_ID, MESSAGE_3_ID);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+        assertFalse(tested.isEmpty());
+        assertFalse(tested.isComplete());
+        assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertEquals(1, (long) tested.firstMessageIndex().orElse(-1L));
+        List<ArchivedMessage> list = tested.list();
+        assertEquals(1, list.size());
+        assertEquals(message2, list.get(0));
+    }
+
+    @Test
+    public void empty() {
+        MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_1_ID, MESSAGE_3_ID);
+        InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit,
+                Collections.emptyList());
+
+        assertTrue(tested.isEmpty());
+        assertTrue(tested.isComplete());
+        assertEquals(0, (long) tested.totalNumberOfMessages().orElse(0L));
+        assertNull(tested.firstMessageIndex().orElse(null));
+        List<ArchivedMessage> list = tested.list();
+        assertTrue(list.isEmpty());
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.java
new file mode 100644
index 0000000..913b23c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveEntityFilterTest {
+
+    @Test
+    public void withMatchingArchiveId() {
+        Entity with = EntityImpl.parseUnchecked("juliet@foo.com");
+        ArchiveEntityFilter tested = new ArchiveEntityFilter(with, with);
+
+        assertEquals(with, tested.entity());
+        assertEquals(EntityFilter.Type.TO_AND_FROM, tested.type());
+        assertTrue(tested.ignoreResource());
+    }
+
+    @Test
+    public void withBeingBareJid() {
+        Entity archiveId = EntityImpl.parseUnchecked("romeo@foo.com");
+        Entity with = EntityImpl.parseUnchecked("juliet@foo.com");
+        ArchiveEntityFilter tested = new ArchiveEntityFilter(archiveId, with);
+
+        assertEquals(with, tested.entity());
+        assertEquals(EntityFilter.Type.TO_OR_FROM, tested.type());
+        assertTrue(tested.ignoreResource());
+    }
+
+    @Test
+    public void withNotMatchingArchiveIdAndNotBeingBareJid() {
+        Entity archiveId = EntityImpl.parseUnchecked("romeo@foo.com");
+        Entity with = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+        ArchiveEntityFilter tested = new ArchiveEntityFilter(archiveId, with);
+
+        assertEquals(with, tested.entity());
+        assertEquals(EntityFilter.Type.TO_OR_FROM, tested.type());
+        assertFalse(tested.ignoreResource());
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java
new file mode 100644
index 0000000..fd03ddc
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java
@@ -0,0 +1,85 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveQueryTest {
+
+    private static final Entity JULIET = EntityImpl.parseUnchecked("juliet@foo.com");
+
+    private static final Entity ARCHIVE_ID = JULIET;
+
+    private MessageArchive archive;
+
+    private X x;
+
+    private QuerySet querySet;
+
+    private ArchiveQuery tested;
+
+    @Before
+    public void before() throws XMLSemanticError {
+        archive = mock(MessageArchive.class);
+        Query query = mock(Query.class);
+        x = mock(X.class);
+        when(query.getX()).thenReturn(x);
+        querySet = mock(QuerySet.class);
+        when(query.getSet()).thenReturn(querySet);
+        tested = new ArchiveQuery(archive, ARCHIVE_ID, query);
+    }
+
+    @Test
+    public void executeWithoutWith() throws XMLSemanticError {
+        when(x.getWith()).thenReturn(Optional.empty());
+
+        ArchivedMessages archivedMessages = mock(ArchivedMessages.class);
+        when(archive.fetchSortedByOldestFirst(any(), any())).thenReturn(archivedMessages);
+
+        assertEquals(archivedMessages, tested.execute());
+    }
+
+    @Test
+    public void executeWithWith() throws XMLSemanticError {
+        when(x.getWith()).thenReturn(Optional.of(JULIET));
+
+        ArchivedMessages archivedMessages = mock(ArchivedMessages.class);
+        when(archive.fetchSortedByOldestFirst(any(), any())).thenReturn(archivedMessages);
+
+        assertEquals(archivedMessages, tested.execute());
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java
new file mode 100644
index 0000000..d0f5592
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java
@@ -0,0 +1,140 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.muc.MUCArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserArchiveQueryHandler;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MAMIQHandlerTest {
+
+    private QueryHandler pubsubNodeArchiveQueryHandler;
+
+    private QueryHandler mucArchiveQueryHandler;
+
+    private QueryHandler userArchiveQueryHandler;
+
+    private IQStanza stanza;
+
+    private ServerRuntimeContext serverRuntimeContext;
+
+    private SessionContext sessionContext;
+
+    private MAMIQHandler tested;
+
+    @Before
+    public void before() throws IOException, SAXException {
+        pubsubNodeArchiveQueryHandler = mock(QueryHandler.class);
+        mucArchiveQueryHandler = mock(MUCArchiveQueryHandler.class);
+        userArchiveQueryHandler = mock(UserArchiveQueryHandler.class);
+
+        tested = new MAMIQHandler("urn:xmpp:mam:2", pubsubNodeArchiveQueryHandler, mucArchiveQueryHandler,
+                userArchiveQueryHandler);
+
+        XMLElement queryIqElement = XMLParserUtil.parseRequiredDocument(
+                "<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+        stanza = new IQStanza(StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build());
+        serverRuntimeContext = mock(ServerRuntimeContext.class);
+        sessionContext = mock(SessionContext.class);
+    }
+
+    @Test
+    public void verifySupportStanza() {
+        assertTrue(tested.verifyInnerElement(stanza));
+    }
+
+    @Test
+    public void verifyUnsupportStanzaNamespace() throws IOException, SAXException {
+        XMLElement queryIqElement = XMLParserUtil
+                .parseRequiredDocument("<iq type='set' id='juliet1'><query queryid='f27'/></iq>");
+        Stanza stanza = StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build();
+
+        assertFalse(tested.verifyInnerElement(stanza));
+    }
+
+    @Test
+    public void verifyUnsupportStanzaName() throws IOException, SAXException {
+        XMLElement queryIqElement = XMLParserUtil
+                .parseRequiredDocument("<iq type='set' id='juliet1'><foo xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+        Stanza stanza = StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build();
+
+        assertFalse(tested.verifyInnerElement(stanza));
+    }
+
+    @Test
+    public void pubsubHasHighestPriority() {
+        when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+        when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+        when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+        tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+        verify(pubsubNodeArchiveQueryHandler).handle(any(), any(), any());
+        verify(mucArchiveQueryHandler, never()).handle(any(), any(), any());
+        verify(userArchiveQueryHandler, never()).handle(any(), any(), any());
+    }
+
+    @Test
+    public void mucHasLessPriorityThanPubsub() {
+        when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+        when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+        when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+        tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+        verify(pubsubNodeArchiveQueryHandler, never()).handle(any(), any(), any());
+        verify(mucArchiveQueryHandler).handle(any(), any(), any());
+        verify(userArchiveQueryHandler, never()).handle(any(), any(), any());
+    }
+
+    @Test
+    public void userHasLowestPriority() {
+        when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+        when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+        when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+        tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+        verify(pubsubNodeArchiveQueryHandler, never()).handle(any(), any(), any());
+        verify(mucArchiveQueryHandler, never()).handle(any(), any(), any());
+        verify(userArchiveQueryHandler).handle(any(), any(), any());
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java
new file mode 100644
index 0000000..4533f57
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java
@@ -0,0 +1,181 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.StanzaAssert;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessages;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MatchingArchivedMessageResultsTest {
+
+    private static final Entity INITIATING_ENTITY = EntityImpl.parseUnchecked("juliet@capulet.lit/chamber");
+
+    private static final Entity ARCHIVE_ID = EntityImpl.parseUnchecked("juliet@capulet.lit");
+
+    private Query query;
+
+    private MessageStanza messageStanza;
+
+    @Before
+    public void before() throws XMLSemanticError, IOException, SAXException {
+        XMLElement queryIqElement = XMLParserUtil.parseRequiredDocument(
+                "<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+        query = new Query("urn:xmpp:mam:2",
+                new IQStanza(StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build()));
+
+        XMLElement messageElement = XMLParserUtil.parseRequiredDocument(
+                "<message xmlns='jabber:client' from=\"witch@shakespeare.lit\" to=\"macbeth@shakespeare.lit\">"
+                        + "<body>Hail to thee</body></message>");
+        messageStanza = new MessageStanza(
+                StanzaBuilder.createClone(messageElement, true, Collections.emptyList()).build());
+    }
+
+    @Test
+    public void testUncomplete() throws IOException, SAXException {
+        SimpleArchivedMessage archivedMessage1 = new SimpleArchivedMessage("28482-98726-73623",
+                ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+        SimpleArchivedMessage archivedMessage2 = new SimpleArchivedMessage("09af3-cc343-b409f",
+                ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+
+        List<ArchivedMessage> archivedMessagesList = new ArrayList<>();
+        archivedMessagesList.add(archivedMessage1);
+        archivedMessagesList.add(archivedMessage2);
+
+        SimpleArchivedMessages archivedMessages = new SimpleArchivedMessages(archivedMessagesList, 0L, 3L);
+
+        MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+                archivedMessages);
+
+        List<Stanza> responseStanzas = tested.toStanzas();
+        assertEquals(3, responseStanzas.size());
+
+        StanzaAssert.assertEquals(StanzaBuilder
+                .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+                        + "  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>"
+                        + "    <forwarded xmlns='urn:xmpp:forward:0'>"
+                        + "      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+                        + "      <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+                        + "        <body>Hail to thee</body>"
+                        + "        <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='28482-98726-73623'/>"
+                        + "</message></forwarded></result></message>"), true, null)
+                .build(), responseStanzas.get(0));
+
+        StanzaAssert.assertEquals(StanzaBuilder
+                .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+                        + "  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='09af3-cc343-b409f'>"
+                        + "    <forwarded xmlns='urn:xmpp:forward:0'>"
+                        + "      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+                        + "      <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+                        + "        <body>Hail to thee</body>"
+                        + "        <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='09af3-cc343-b409f'/>"
+                        + "</message></forwarded></result></message>"), true, null)
+                .build(), responseStanzas.get(1));
+
+        StanzaAssert.assertEquals(
+                StanzaBuilder.createClone(XMLParserUtil
+                        .parseRequiredDocument("<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2'>"
+                                + "    <set xmlns='http://jabber.org/protocol/rsm'>"
+                                + "      <count>3</count><first index='0'>28482-98726-73623</first>"
+                                + "      <last>09af3-cc343-b409f</last></set></fin></iq>"),
+                        true, null).build(),
+                responseStanzas.get(2));
+    }
+
+    @Test
+    public void testComplete() throws IOException, SAXException {
+        ArchivedMessage message = new SimpleArchivedMessage("28482-98726-73623",
+                ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+
+        ArchivedMessages archivedMessages = new SimpleArchivedMessages(Collections.singletonList(message), 0L, 1L);
+
+        MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+                archivedMessages);
+
+        List<Stanza> responseStanzas = tested.toStanzas();
+        assertEquals(2, responseStanzas.size());
+
+        StanzaAssert.assertEquals(StanzaBuilder
+                .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+                        + "  <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>"
+                        + "    <forwarded xmlns='urn:xmpp:forward:0'>"
+                        + "      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+                        + "      <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+                        + "        <body>Hail to thee</body>"
+                        + "        <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='28482-98726-73623'/>"
+                        + "</message></forwarded></result></message>"), true, null)
+                .build(), responseStanzas.get(0));
+
+        StanzaAssert.assertEquals(
+                StanzaBuilder.createClone(XMLParserUtil.parseRequiredDocument(
+                        "<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2' complete='true'>"
+                                + "    <set xmlns='http://jabber.org/protocol/rsm'>"
+                                + "      <count>1</count><first index='0'>28482-98726-73623</first>"
+                                + "      <last>28482-98726-73623</last></set></fin></iq>"),
+                        true, null).build(),
+                responseStanzas.get(1));
+    }
+
+    @Test
+    public void testEmptyPage() throws IOException, SAXException {
+        SimpleArchivedMessages archivedMessages = new SimpleArchivedMessages(Collections.emptyList(), 0L, 50L);
+
+        MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+                archivedMessages);
+
+        List<Stanza> responseStanzas = tested.toStanzas();
+        assertEquals(1, responseStanzas.size());
+
+        StanzaAssert.assertEquals(
+                StanzaBuilder.createClone(XMLParserUtil
+                        .parseRequiredDocument("<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2'>"
+                                + "    <set xmlns='http://jabber.org/protocol/rsm'>"
+                                + "      <count>50</count></set></fin></iq>"),
+                        true, null).build(),
+                responseStanzas.get(0));
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java
new file mode 100644
index 0000000..7258208
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java
@@ -0,0 +1,58 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class QuerySetTest {
+
+    @Test
+    public void mainCase() throws XMLSemanticError {
+        Set set = Set.builder().max(10L).after("first").before("last").build();
+        QuerySet tested = new QuerySet(set);
+        assertEquals(10L, (long) tested.pageSize().orElse(0L));
+        assertEquals("first", tested.firstMessageId().orElse(null));
+        assertEquals("last", tested.lastMessageId().orElse(null));
+    }
+
+    @Test
+    public void maxAndFirstMessageIdBlankIsALastPageQuery() throws XMLSemanticError {
+        Set set = Set.builder().max(10L).before("").build();
+        QuerySet tested = new QuerySet(set);
+        assertTrue(tested.lastPage());
+    }
+
+    @Test
+    public void maxAndFirstMessageIdBlankAndLastMessageIdNotBlankIsNotALastPageQuery() throws XMLSemanticError {
+        Set set = Set.builder().max(10L).after("first").before("").build();
+        QuerySet tested = new QuerySet(set);
+        assertFalse(tested.lastPage());
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java
new file mode 100644
index 0000000..48b20dd
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java
@@ -0,0 +1,65 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MessageArchiveMock implements MessageArchive {
+
+    private final Queue<Message> messages = new LinkedList<>();
+
+    @Override
+    public void archive(Message message) {
+        messages.add(message);
+    }
+
+    @Override
+    public ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest) {
+        return new SimpleArchivedMessages(Collections.emptyList(), 0L, 0L);
+    }
+
+    @Override
+    public ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize) {
+        return new SimpleArchivedMessages(Collections.emptyList(), 0L, 0L);
+    }
+
+    public void clear() {
+        messages.clear();
+    }
+
+    public void assertUniqueArchivedMessageStanza(MessageStanza messageStanza) {
+        assertEquals(1, messages.size());
+        assertEquals(messageStanza, messages.peek().stanza());
+    }
+
+    public void assertEmpty() {
+        assertTrue(messages.isEmpty());
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java
new file mode 100644
index 0000000..b484fe9
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java
@@ -0,0 +1,45 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MessageArchivesMock implements MessageArchives {
+
+    private final Map<Entity, MessageArchive> archiveById = new HashMap<>();
+
+    public MessageArchiveMock givenArchive(Entity archiveId) {
+        MessageArchiveMock userMessageArchiveMock = new MessageArchiveMock();
+        archiveById.put(archiveId, userMessageArchiveMock);
+        return userMessageArchiveMock;
+    }
+
+    @Override
+    public Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid) {
+        return Optional.ofNullable(archiveById.get(userBareJid));
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java
new file mode 100644
index 0000000..78305ce
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java
@@ -0,0 +1,57 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleEntityFilter implements EntityFilter {
+
+    private final Entity entity;
+
+    private final Type type;
+
+    private final boolean ignoreResource;
+
+    public SimpleEntityFilter(Entity entity, Type type, boolean ignoreResource) {
+        this.entity = requireNonNull(entity);
+        this.type = requireNonNull(type);
+        this.ignoreResource = ignoreResource;
+    }
+
+    @Override
+    public Entity entity() {
+        return entity;
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public boolean ignoreResource() {
+        return ignoreResource;
+    }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java
new file mode 100644
index 0000000..585e7ce
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java
@@ -0,0 +1,58 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleMessagePageRequest implements MessagePageRequest {
+
+    private final Long pageSize;
+
+    private final String firstMessageId;
+
+    private final String lastMessageId;
+
+    public SimpleMessagePageRequest(Long pageSize, String firstMessageId, String lastMessageId) {
+        this.pageSize = pageSize;
+        this.firstMessageId = firstMessageId;
+        this.lastMessageId = lastMessageId;
+    }
+
+    @Override
+    public Optional<Long> pageSize() {
+        return Optional.ofNullable(pageSize);
+    }
+
+    @Override
+    public Optional<String> firstMessageId() {
+        return Optional.ofNullable(firstMessageId).filter(StringUtils::isNotBlank);
+    }
+
+    @Override
+    public Optional<String> lastMessageId() {
+        return Optional.ofNullable(lastMessageId).filter(StringUtils::isNotBlank);
+    }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java
new file mode 100644
index 0000000..0150861
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java
@@ -0,0 +1,124 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.ServerRuntimeContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SessionContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveQueryHandlerTest {
+
+    private static final Entity JULIET = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+
+    private static final Entity ROMEO = EntityImpl.parseUnchecked("romeo@foo.com/floor");
+
+    private static final Entity INITIATING_ENTITY = JULIET;
+
+    private Query romeoTargetingQuery;
+
+    private Query untargetedQuery;
+
+    private MessageArchivesMock archives;
+
+    private ServerRuntimeContextMock serverRuntimeContext;
+
+    private SessionContextMock sessionContext;
+
+    private UserArchiveQueryHandler tested;
+
+    @Before
+    public void before() throws IOException, SAXException, XMLSemanticError {
+        romeoTargetingQuery = new Query("urn:xmpp:mam:2",
+                new IQStanza(StanzaBuilder.createClone(
+                        XMLParserUtil.parseRequiredDocument(
+                                "<iq type='set' to='romeo@foo.com'><query xmlns='urn:xmpp:mam:2'/></iq>"),
+                        true, Collections.emptyList()).build()));
+
+        untargetedQuery = new Query("urn:xmpp:mam:2",
+                new IQStanza(StanzaBuilder.createClone(
+                        XMLParserUtil.parseRequiredDocument("<iq type='set'><query xmlns='urn:xmpp:mam:2'/></iq>"),
+                        true, Collections.emptyList()).build()));
+
+        serverRuntimeContext = new ServerRuntimeContextMock();
+        archives = serverRuntimeContext.givenUserMessageArchives();
+        sessionContext = new SessionContextMock();
+        sessionContext.givenInitiatingEntity(INITIATING_ENTITY);
+        tested = new UserArchiveQueryHandler();
+    }
+
+    @Test
+    public void supportsAllQueries() {
+        assertTrue(tested.supports(null, null, null));
+    }
+
+    @Test
+    public void untargetedQueryEndsUpInInitiatingEntityArchive() throws XMLSemanticError {
+        archives.givenArchive(INITIATING_ENTITY.getBareJID());
+        List<Stanza> stanzas = tested.handle(untargetedQuery, serverRuntimeContext, sessionContext);
+        assertEquals(1, stanzas.size());
+        Stanza stanza = stanzas.get(0);
+        assertEquals("iq", stanza.getName());
+        assertNotNull(stanza.getSingleInnerElementsNamed("fin"));
+    }
+
+    @Test
+    public void preventsUserFromQueryingOtherUserArchive() throws XMLSemanticError {
+        List<Stanza> stanzas = tested.handle(romeoTargetingQuery, serverRuntimeContext, sessionContext);
+        assertError(stanzas, StanzaErrorCondition.FORBIDDEN);
+    }
+
+    @Test
+    public void unexistingArchiveLeadsToItemNotFound() throws XMLSemanticError {
+        List<Stanza> stanzas = tested.handle(untargetedQuery, serverRuntimeContext, sessionContext);
+        assertError(stanzas, StanzaErrorCondition.ITEM_NOT_FOUND);
+    }
+
+    private void assertError(List<Stanza> stanzas, StanzaErrorCondition errorCondition) throws XMLSemanticError {
+        assertEquals(1, stanzas.size());
+        XMLElement error = stanzas.get(0).getSingleInnerElementsNamed("error");
+        assertNotNull(error);
+        assertNotNull(error.getSingleInnerElementsNamed(errorCondition.value()));
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java
new file mode 100644
index 0000000..a70ddf7
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java
@@ -0,0 +1,155 @@
+/*
+ *  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.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.stream.Stream;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.ServerRuntimeContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SessionContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchiveMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanzaType;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserMessageListenerTest {
+
+    private static final Entity JULIET_IN_CHAMBER = EntityImpl.parseUnchecked("juliet@capulet.lit/chamber");
+
+    private static final Entity ROMEO_IN_ORCHARD = EntityImpl.parseUnchecked("romeo@montague.lit/orchard");
+
+    private static final Entity MACBETH_IN_KITCHEN = EntityImpl.parseUnchecked("macbeth@shakespeare.lit/kitchen");
+
+    private static final Entity ALICE_IN_RABBIT_HOLE = EntityImpl.parseUnchecked("alice@carol.lit/rabbit-hole");
+
+    private static final Entity INITIATING_ENTITY = JULIET_IN_CHAMBER;
+
+    private MessageArchiveMock julietArchive;
+
+    private MessageArchiveMock romeoArchive;
+
+    private MessageArchiveMock macbethArchive;
+
+    private AcceptedMessageEvent acceptedMessage;
+
+    private MessageStanza messageStanza;
+
+    private UserMessageListener tested;
+
+    @Before
+    public void before() {
+        ServerRuntimeContextMock serverRuntimeContext = new ServerRuntimeContextMock();
+
+        MessageArchivesMock archives = serverRuntimeContext.givenUserMessageArchives();
+
+        julietArchive = archives.givenArchive(JULIET_IN_CHAMBER.getBareJID());
+        romeoArchive = archives.givenArchive(ROMEO_IN_ORCHARD.getBareJID());
+        macbethArchive = archives.givenArchive(MACBETH_IN_KITCHEN.getBareJID());
+
+        SessionContextMock sessionContext = new SessionContextMock();
+        sessionContext.givenInitiatingEntity(INITIATING_ENTITY);
+        sessionContext.givenServerRuntimeContext(serverRuntimeContext);
+
+        messageStanza = mock(MessageStanza.class);
+        when(messageStanza.getMessageType()).thenReturn(MessageStanzaType.NORMAL);
+        acceptedMessage = mock(AcceptedMessageEvent.class);
+        when(acceptedMessage.serverRuntimeContext()).thenReturn(serverRuntimeContext);
+        when(acceptedMessage.sessionContext()).thenReturn(sessionContext);
+        when(acceptedMessage.messageStanza()).thenReturn(messageStanza);
+
+        tested = new UserMessageListener();
+    }
+
+    @Test
+    public void onlyNormalAndChatMessageAreArchived() {
+        when(acceptedMessage.isOutbound()).thenReturn(true);
+
+        Stream.of(MessageStanzaType.values()).filter(messageStanzaType -> messageStanzaType != MessageStanzaType.NORMAL)
+                .filter(messageStanzaType -> messageStanzaType != MessageStanzaType.CHAT).forEach(messageStanzaType -> {
+                    when(messageStanza.getMessageType()).thenReturn(messageStanzaType);
+                    tested.onEvent(acceptedMessage);
+
+                    julietArchive.assertEmpty();
+                    romeoArchive.assertEmpty();
+                    macbethArchive.assertEmpty();
+                });
+
+        Stream.of(MessageStanzaType.CHAT, MessageStanzaType.NORMAL).forEach(messageStanzaType -> {
+            julietArchive.clear();
+
+            when(messageStanza.getMessageType()).thenReturn(messageStanzaType);
+            tested.onEvent(acceptedMessage);
+
+            julietArchive.assertUniqueArchivedMessageStanza(messageStanza);
+        });
+    }
+
+    @Test
+    public void outboundMessageHavingFrom() {
+        when(acceptedMessage.isOutbound()).thenReturn(true);
+        when(messageStanza.getFrom()).thenReturn(ROMEO_IN_ORCHARD);
+
+        tested.onEvent(acceptedMessage);
+
+        romeoArchive.assertUniqueArchivedMessageStanza(messageStanza);
+    }
+
+    @Test
+    public void outboundMessageWithoutFrom() {
+        when(acceptedMessage.isOutbound()).thenReturn(true);
+
+        tested.onEvent(acceptedMessage);
+
+        julietArchive.assertUniqueArchivedMessageStanza(messageStanza);
+    }
+
+    @Test
+    public void inboundMessage() {
+        when(acceptedMessage.isOutbound()).thenReturn(false);
+        when(messageStanza.getTo()).thenReturn(MACBETH_IN_KITCHEN);
+
+        tested.onEvent(acceptedMessage);
+
+        macbethArchive.assertUniqueArchivedMessageStanza(messageStanza);
+    }
+
+    @Test
+    public void unexistingArchive() {
+        when(acceptedMessage.isOutbound()).thenReturn(true);
+        when(messageStanza.getFrom()).thenReturn(ALICE_IN_RABBIT_HOLE);
+
+        tested.onEvent(acceptedMessage);
+
+        julietArchive.assertEmpty();
+        romeoArchive.assertEmpty();
+        macbethArchive.assertEmpty();
+    }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert b/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert
new file mode 100644
index 0000000..5a8e025
Binary files /dev/null and b/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert differ