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:15 UTC

[mina-vysper] branch xep-0313 updated (2eb9b00 -> aff6f45)

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

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


 discard 2eb9b00  XEP-0313 Message Archive Management
     add ffce856  In StanzaHandler, use StanzaBroker.writeToSession instead of SessionContext.getResponseWriter
     add e843cb2  Remove ResponseStanzaContainer
     add d6a9cb3  Hide session stanza writer in a StanzaReceivingSessionContext sub-interface
     new aff6f45  XEP-0313 Message Archive Management

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (2eb9b00)
            \
             N -- N -- N   refs/heads/xep-0313 (aff6f45)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/vysper/mina/XmppIoHandlerAdapter.java   | 44 +++++++-------
 .../vysper/xmpp/delivery/LocalDeliveryUtils.java   | 19 +++---
 .../vysper/xmpp/delivery/RecordingStanzaRelay.java |  4 +-
 .../vysper/xmpp/delivery/StanzaReceiverRelay.java  |  6 +-
 .../apache/vysper/xmpp/delivery/StanzaRelay.java   |  5 +-
 .../vysper/xmpp/delivery/StanzaRelayBroker.java    |  4 +-
 .../DeliveringExternalInboundStanzaRelay.java      |  4 +-
 .../DeliveringInternalInboundStanzaRelay.java      | 22 +++----
 .../modules/core/base/handler/MessageHandler.java  |  6 +-
 .../core/base/handler/RelayingIQHandler.java       |  2 +-
 .../core/base/handler/async/ResponseFuture.java    |  5 --
 .../im/handler/PresenceAvailabilityHandler.java    |  5 +-
 .../im/handler/PresenceSubscriptionHandler.java    |  2 +-
 .../modules/core/sasl/handler/AbortHandler.java    |  4 +-
 .../xep0220_server_dailback/DbVerifyHandler.java   |  2 +-
 .../handler/DiscoInfoIQHandler.java                |  6 +-
 .../vysper/xmpp/protocol/ProtocolWorker.java       | 22 +++----
 .../xmpp/protocol/QueuedStanzaProcessor.java       | 15 ++---
 .../xmpp/protocol/ResponseStanzaContainer.java     | 45 --------------
 .../xmpp/protocol/ResponseStanzaContainerImpl.java | 69 ----------------------
 .../vysper/xmpp/protocol/ResponseWriter.java       | 57 +++++-------------
 .../vysper/xmpp/protocol/SimpleStanzaBroker.java   |  6 +-
 .../xmpp/protocol/SimpleStanzaHandlerExecutor.java |  4 +-
 .../apache/vysper/xmpp/protocol/StanzaHandler.java |  7 +--
 .../xmpp/protocol/StanzaHandlerExecutor.java       |  4 +-
 .../vysper/xmpp/protocol/StanzaProcessor.java      |  7 ++-
 .../xmpp/protocol/StateAwareProtocolWorker.java    |  6 +-
 .../worker/AbstractStateAwareProtocolWorker.java   | 13 ++--
 .../worker/AuthenticatedProtocolWorker.java        |  6 +-
 .../protocol/worker/EncryptedProtocolWorker.java   |  6 +-
 .../worker/EncryptionStartedProtocolWorker.java    |  6 +-
 .../protocol/worker/EndOrClosedProtocolWorker.java |  6 +-
 .../worker/InboundStanzaProtocolWorker.java        |  3 -
 .../protocol/worker/InitiatedProtocolWorker.java   |  6 +-
 .../protocol/worker/StartedProtocolWorker.java     |  6 +-
 .../protocol/worker/UnconnectedProtocolWorker.java |  6 +-
 .../vysper/xmpp/server/AbstractSessionContext.java |  2 +-
 .../apache/vysper/xmpp/server/SessionContext.java  | 55 +++++++++--------
 .../server/StanzaReceivingSessionContext.java}     | 11 ++--
 .../components/ComponentStanzaProcessor.java       |  8 +--
 .../resourcebinding/DefaultResourceRegistry.java   | 21 +++----
 .../state/resourcebinding/ResourceRegistry.java    | 11 ++--
 .../xmpp/delivery/StanzaRelayBrokerTestCase.java   |  3 +-
 ...liveringExternalInboundStanzaRelayTestCase.java |  3 +-
 ...eliveringInteralInboundStanzaRelayTestCase.java |  3 +-
 .../base/handler/RelayingIQHandlerTestCase.java    |  4 +-
 .../core/bind/handler/BindIQHandlerTestCase.java   |  8 +--
 .../PresenceAvailInitialOutHandlerTestCase.java    |  7 +--
 .../SoftwareVersionIQHandlerTestCase.java          |  3 +-
 .../EntityTimeIQHandlerTestCase.java               |  3 +-
 .../EntityTimeXEP0090IQHandlerTestCase.java        |  3 +-
 .../handler/DiscoInfoIQHandlerTestCase.java        | 39 ++++++------
 .../handler/ExtendedDiscoInfoTestCase.java         | 16 +++--
 .../protocol/CallTestStanzaHandlerResponse.java    |  6 +-
 .../ComponentStanzaProcessorTestCase.java          |  3 +-
 .../resourcebinding/ResourceRegistryTestCase.java  |  7 ++-
 .../websockets/JettyXmppWebSocketTest.java         |  3 +-
 .../websockets/TomcatXmppWebSocketTest.java        |  3 +-
 .../AbstractMUCOccupantDiscoTestCase.java          |  1 -
 .../PubSubRetrieveAffiliationsTestCase.java        |  1 -
 .../xep0065_socks/Socks5IqHandlerTest.java         |  7 +--
 .../xmpp/extension/xep0124/BoshHandlerTest.java    | 11 ++--
 62 files changed, 266 insertions(+), 416 deletions(-)
 delete mode 100644 server/core/src/main/java/org/apache/vysper/xmpp/protocol/ResponseStanzaContainer.java
 delete mode 100644 server/core/src/main/java/org/apache/vysper/xmpp/protocol/ResponseStanzaContainerImpl.java
 copy server/core/src/main/java/org/apache/vysper/{event/EventBus.java => xmpp/server/StanzaReceivingSessionContext.java} (81%)


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

Posted by ra...@apache.org.
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