You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by ka...@apache.org on 2022/01/31 14:20:19 UTC

[james-project] branch master updated (4fad67e -> 52e194b)

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

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


    from 4fad67e  JAMES-3693 Integration Tests for GlobalRateLimit and PerSenderRateLimit (#869)
     new 2bc1476  JAMES-3709 Concurrent message deletion can cause issue in POP3
     new 52e194b  JAMES-3709 Refactoring POP3 command handlers to share common code. Also fixes a disabled POP3ServerTest.

The 2 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:
 .../james/protocols/pop3/core/DeleCmdHandler.java  |  61 +++------
 .../james/protocols/pop3/core/ListCmdHandler.java  |  82 +++++-------
 .../protocols/pop3/core/MessageMetaDataUtils.java  |  52 --------
 .../pop3/core/POP3MessageCommandArguments.java     |  74 +++++++++++
 .../pop3/core/POP3MessageCommandDelegate.java      |  87 +++++++++++++
 .../james/protocols/pop3/core/RetrCmdHandler.java  |  67 ++--------
 .../james/protocols/pop3/core/StatCmdHandler.java  |   6 +-
 .../james/protocols/pop3/core/TopCmdHandler.java   |  86 ++++---------
 .../james/protocols/pop3/core/UidlCmdHandler.java  |  66 ++++------
 .../james/protocols/pop3/mailbox/Mailbox.java      |   6 +-
 .../protocols/pop3/mailbox/MessageMetaData.java    |   6 +-
 .../protocols/pop3/core/DeleCmdHandlerTest.java    | 115 +++++++++++++++++
 .../protocols/pop3/core/ListCmdHandlerTest.java    | 124 ++++++++++++++++++
 .../pop3/core/POP3MessageCommandArgumentsTest.java | 119 ++++++++++++++++++
 .../pop3/core/POP3MessageCommandDelegateTest.java  | 132 +++++++++++++++++++
 .../protocols/pop3/core/RetrCmdHandlerTest.java    |  51 +++++---
 .../protocols/pop3/core/TopCmdHandlerTest.java     | 125 ++++++++++++++++++
 .../protocols/pop3/core/UidlCmdHandlerTest.java    | 139 +++++++++++++++++++++
 .../james/protocols/pop3/utils/MockMailbox.java    |   5 +-
 .../mailbox/DistributedMailboxAdapter.java         |   2 +-
 .../james/pop3server/mailbox/MailboxAdapter.java   |   2 +-
 .../apache/james/pop3server/POP3ServerTest.java    |  48 +++++++
 22 files changed, 1111 insertions(+), 344 deletions(-)
 delete mode 100644 protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/MessageMetaDataUtils.java
 create mode 100644 protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArguments.java
 create mode 100644 protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegate.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/DeleCmdHandlerTest.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/ListCmdHandlerTest.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArgumentsTest.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegateTest.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/TopCmdHandlerTest.java
 create mode 100644 protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/UidlCmdHandlerTest.java

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


[james-project] 01/02: JAMES-3709 Concurrent message deletion can cause issue in POP3

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 2bc147659e85db43089a74107057fb9c5db37f0a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Jan 26 18:13:27 2022 +0700

    JAMES-3709 Concurrent message deletion can cause issue in POP3
---
 .../apache/james/pop3server/POP3ServerTest.java    | 53 ++++++++++++++++++++++
 1 file changed, 53 insertions(+)

diff --git a/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java b/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
index 6cef267..4302476 100644
--- a/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
+++ b/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
@@ -19,6 +19,7 @@
 package org.apache.james.pop3server;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.ByteArrayOutputStream;
@@ -34,6 +35,7 @@ import java.util.concurrent.TimeUnit;
 import javax.mail.util.SharedByteArrayInputStream;
 
 import org.apache.commons.configuration2.XMLConfiguration;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.net.pop3.POP3Client;
 import org.apache.commons.net.pop3.POP3MessageInfo;
 import org.apache.commons.net.pop3.POP3Reply;
@@ -48,6 +50,7 @@ import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
 import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MessageRange;
 import org.apache.james.mailbox.store.StoreMailboxManager;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
@@ -73,6 +76,8 @@ import org.junit.jupiter.api.Test;
 
 import com.google.inject.name.Names;
 
+import reactor.core.publisher.Flux;
+
 public class POP3ServerTest {
     private static final DomainList NO_DOMAIN_LIST = null;
 
@@ -544,6 +549,54 @@ public class POP3ServerTest {
         mailboxManager.deleteMailbox(mailboxPath, session);
     }
 
+    @Disabled("JAMES-3709 Concurrent deletes causes NPE when retrieving messages" +
+        "POP3: UIDL" +
+        "Delete the messages" +
+        "POP3: RETR XXX" +
+        "   /!\\ NPE")
+    @Test
+    void pop3SessionShouldTolerateConcurrentDeletes() throws Exception {
+        finishSetUp(pop3Configuration);
+
+        pop3Client = new POP3Client();
+        InetSocketAddress bindedAddress = new ProtocolServerUtils(pop3Server).retrieveBindedAddress();
+        pop3Client.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort());
+
+        Username username = Username.of("foo2");
+        usersRepository.addUser(username, "bar2");
+
+        MailboxPath mailboxPath = MailboxPath.inbox(username);
+        MailboxSession session = mailboxManager.login(username, "bar2");
+
+        if (!mailboxManager.mailboxExists(mailboxPath, session).block()) {
+            mailboxManager.createMailbox(mailboxPath, session);
+        }
+
+        setupTestMails(session, mailboxManager.getMailbox(mailboxPath, session));
+
+        pop3Client.login("foo2", "bar2");
+        assertThat(pop3Client.getState()).isEqualTo(1);
+
+        POP3MessageInfo[] entries = pop3Client.listMessages();
+
+        assertThat(entries).isNotNull();
+        assertThat(entries.length).isEqualTo(2);
+        assertThat(pop3Client.getState()).isEqualTo(1);
+
+        mailboxManager.getMailbox(mailboxPath, session).delete(
+            Flux.from(mailboxManager.getMailbox(mailboxPath, session).listMessagesMetadata(MessageRange.all(), session))
+            .map(i -> i.getComposedMessageId().getUid())
+            .collectList().block(),
+            session);
+
+        Reader r = pop3Client.retrieveMessageTop(entries[0].number, 0);
+
+        assertThat(r).isNull();
+
+        // Fails: The NPE is not handled and causes the connection to abort...
+        assertThatCode(() -> pop3Client.listMessages()).doesNotThrowAnyException();
+    }
+
     /**
      * Test for JAMES-1202 -  Which shows that UIDL,STAT and LIST all show the same message numbers.
      */

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


[james-project] 02/02: JAMES-3709 Refactoring POP3 command handlers to share common code. Also fixes a disabled POP3ServerTest.

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 52e194b989741cf80170266e5df9cafa7e78ddcc
Author: Karsten Otto <ka...@akquinet.de>
AuthorDate: Mon Jan 24 15:42:28 2022 +0100

    JAMES-3709 Refactoring POP3 command handlers to share common code.
    Also fixes a disabled POP3ServerTest.
---
 .../james/protocols/pop3/core/DeleCmdHandler.java  |  61 +++------
 .../james/protocols/pop3/core/ListCmdHandler.java  |  82 +++++-------
 .../protocols/pop3/core/MessageMetaDataUtils.java  |  52 --------
 .../pop3/core/POP3MessageCommandArguments.java     |  74 +++++++++++
 .../pop3/core/POP3MessageCommandDelegate.java      |  87 +++++++++++++
 .../james/protocols/pop3/core/RetrCmdHandler.java  |  67 ++--------
 .../james/protocols/pop3/core/StatCmdHandler.java  |   6 +-
 .../james/protocols/pop3/core/TopCmdHandler.java   |  86 ++++---------
 .../james/protocols/pop3/core/UidlCmdHandler.java  |  66 ++++------
 .../james/protocols/pop3/mailbox/Mailbox.java      |   6 +-
 .../protocols/pop3/mailbox/MessageMetaData.java    |   6 +-
 .../protocols/pop3/core/DeleCmdHandlerTest.java    | 115 +++++++++++++++++
 .../protocols/pop3/core/ListCmdHandlerTest.java    | 124 ++++++++++++++++++
 .../pop3/core/POP3MessageCommandArgumentsTest.java | 119 ++++++++++++++++++
 .../pop3/core/POP3MessageCommandDelegateTest.java  | 132 +++++++++++++++++++
 .../protocols/pop3/core/RetrCmdHandlerTest.java    |  51 +++++---
 .../protocols/pop3/core/TopCmdHandlerTest.java     | 125 ++++++++++++++++++
 .../protocols/pop3/core/UidlCmdHandlerTest.java    | 139 +++++++++++++++++++++
 .../james/protocols/pop3/utils/MockMailbox.java    |   5 +-
 .../mailbox/DistributedMailboxAdapter.java         |   2 +-
 .../james/pop3server/mailbox/MailboxAdapter.java   |   2 +-
 .../apache/james/pop3server/POP3ServerTest.java    |   9 +-
 22 files changed, 1065 insertions(+), 351 deletions(-)

diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/DeleCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/DeleCmdHandler.java
index 6cd388b..535951c 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/DeleCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/DeleCmdHandler.java
@@ -42,14 +42,28 @@ import com.google.common.collect.ImmutableSet;
 public class DeleCmdHandler extends AbstractPOP3CommandHandler {
     private static final Collection<String> COMMANDS = ImmutableSet.of("DELE");
 
-    private static final Response SYNTAX_ERROR = new POP3Response(POP3Response.ERR_RESPONSE, "Usage: DELE [mail number]").immutable();
     private static final Response DELETED = new POP3Response(POP3Response.OK_RESPONSE, "Message deleted").immutable();
 
     private final MetricFactory metricFactory;
-
+    private final POP3MessageCommandDelegate commandDelegate;
+    
     @Inject
     public DeleCmdHandler(MetricFactory metricFactory) {
         this.metricFactory = metricFactory;
+        this.commandDelegate = new POP3MessageCommandDelegate(COMMANDS) {
+            @Override
+            protected Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) {
+                List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction)
+                    .orElseGet(() -> {
+                        ArrayList<String> uidList = new ArrayList<>();
+                        session.setAttachment(POP3Session.DELETED_UID_LIST, uidList, State.Transaction);
+                        return uidList;
+                    });
+
+                deletedUidList.add(data.getUid());
+                return DELETED;
+            }
+        };
     }
 
     /**
@@ -64,48 +78,7 @@ public class DeleCmdHandler extends AbstractPOP3CommandHandler {
                     .addToContext(MDCBuilder.ACTION, "DELE")
                     .addToContext(MDCConstants.withSession(session))
                     .addToContext(MDCConstants.forRequest(request)),
-                () -> delete(session, request)));
-    }
-
-    private Response delete(POP3Session session, Request request) {
-        if (session.getHandlerState() == POP3Session.TRANSACTION) {
-            int num = 0;
-            try {
-                num = Integer.parseInt(request.getArgument());
-            } catch (Exception e) {
-                return SYNTAX_ERROR;
-            }
-            try {
-                MessageMetaData meta = MessageMetaDataUtils.getMetaData(session, num);
-                if (meta == null) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                    return  new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                }
-                List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction)
-                    .orElseGet(() -> {
-                        ArrayList<String> uidList = new ArrayList<>();
-                        session.setAttachment(POP3Session.DELETED_UID_LIST, uidList, State.Transaction);
-                        return uidList;
-                    });
-
-                String uid = meta.getUid();
-
-                if (deletedUidList.contains(uid)) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") already deleted.");
-                    return new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                } else {
-                    deletedUidList.add(uid);
-                    // we are replacing our reference with "DELETED", so we have
-                    // to dispose the no-more-referenced mail object.
-                    return DELETED;
-                }
-            } catch (IndexOutOfBoundsException iob) {
-                StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                return  new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-            }
-        } else {
-            return POP3Response.ERR;
-        }
+                () -> commandDelegate.handleMessageRequest(session, request)));
     }
 
     @Override
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/ListCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/ListCmdHandler.java
index 1543152..bf15ce9 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/ListCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/ListCmdHandler.java
@@ -45,10 +45,17 @@ public class ListCmdHandler extends AbstractPOP3CommandHandler {
     private static final Collection<String> COMMANDS = ImmutableSet.of("LIST");
 
     private final MetricFactory metricFactory;
+    private final POP3MessageCommandDelegate commandDelegate;
 
     @Inject
     public ListCmdHandler(MetricFactory metricFactory) {
         this.metricFactory = metricFactory;
+        this.commandDelegate = new POP3MessageCommandDelegate(COMMANDS) {
+            @Override
+            protected POP3Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) {
+                return new POP3Response(POP3Response.OK_RESPONSE, args.getMessageNumber() + " " + data.getSize());
+            }
+        };
     }
 
     /**
@@ -70,66 +77,33 @@ public class ListCmdHandler extends AbstractPOP3CommandHandler {
                     .addToContext(MDCBuilder.ACTION, "LIST")
                     .addToContext(MDCConstants.withSession(session))
                     .addToContext(MDCConstants.forRequest(request)),
-                () -> list(session, request)));
+                () -> handleMessageRequest(session, request)));
     }
 
-    private Response list(POP3Session session, Request request) {
-        String parameters = request.getArgument();
-        List<MessageMetaData> uidList = session.getAttachment(POP3Session.UID_LIST, State.Transaction).orElse(ImmutableList.of());
-        List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction).orElse(ImmutableList.of());
+    private Response handleMessageRequest(POP3Session session, Request request) {
+        if (request.getArgument() != null) {
+            return commandDelegate.handleMessageRequest(session, request);
+        }
 
         if (session.getHandlerState() == POP3Session.TRANSACTION) {
-            POP3Response response = null;
-
-            if (parameters == null) {
-
-                long size = 0;
-                int count = 0;
-                List<MessageMetaData> validResults = new ArrayList<>();
-                if (!uidList.isEmpty()) {
-
-                    for (MessageMetaData data : uidList) {
-                        if (!deletedUidList.contains(data.getUid())) {
-                            size += data.getSize();
-                            count++;
-                            validResults.add(data);
-                        }
-                    }
-                }
-                StringBuilder responseBuffer = new StringBuilder(32).append(count).append(" ").append(size);
-                response = new POP3Response(POP3Response.OK_RESPONSE, responseBuffer.toString());
-                count = 0;
-                for (int i = 0; i < validResults.size(); i++) {
-                    responseBuffer = new StringBuilder(16).append(i + 1).append(" ").append(validResults.get(i).getSize());
-                    response.appendLine(responseBuffer.toString());
-                }
-                response.appendLine(".");
-            } else {
-                int num = 0;
-                try {
-                    num = Integer.parseInt(parameters);
-
-                    MessageMetaData data = MessageMetaDataUtils.getMetaData(session, num);
-                    if (data == null) {
-                        StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                        return  new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                    }
-
-                    if (!deletedUidList.contains(data.getUid())) {
-                        StringBuilder responseBuffer = new StringBuilder(64).append(num).append(" ").append(data.getSize());
-                        response = new POP3Response(POP3Response.OK_RESPONSE, responseBuffer.toString());
-                    } else {
-                        StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") already deleted.");
-                        response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                    }
-                } catch (IndexOutOfBoundsException npe) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                    response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                } catch (NumberFormatException nfe) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append(parameters).append(" is not a valid number");
-                    response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
+
+            List<MessageMetaData> uidList = session.getAttachment(POP3Session.UID_LIST, State.Transaction).orElse(ImmutableList.of());
+            List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction).orElse(ImmutableList.of());
+
+            long totalSize = 0;
+            List<String> validResults = new ArrayList<>();
+            for (int i = 0; i < uidList.size(); i++) {
+                MessageMetaData data = uidList.get(i);
+                if (!deletedUidList.contains(data.getUid())) {
+                    totalSize += data.getSize();
+                    validResults.add((i + 1) + " " + data.getSize());
                 }
             }
+
+            POP3Response response = new POP3Response(POP3Response.OK_RESPONSE, validResults.size() + " " + totalSize);
+            validResults.forEach(response::appendLine);
+            response.appendLine(".");
+
             return response;
         } else {
             return POP3Response.ERR;
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/MessageMetaDataUtils.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/MessageMetaDataUtils.java
deleted file mode 100644
index 339c758..0000000
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/MessageMetaDataUtils.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.protocols.pop3.core;
-
-import java.util.stream.IntStream;
-
-import org.apache.james.protocols.api.ProtocolSession.State;
-import org.apache.james.protocols.pop3.POP3Session;
-import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
-
-public class MessageMetaDataUtils {
-
-    /**
-     * Returns the {@link MessageMetaData} for the given message number or <code>null</code> if it can not be 
-     * found.
-     */
-    public static MessageMetaData getMetaData(POP3Session session, int number) {
-        return session.getAttachment(POP3Session.UID_LIST, State.Transaction)
-            .filter(uidList -> number <= uidList.size())
-            .map(uidList -> uidList.get(number - 1))
-            .orElse(null);
-    }
-
-    /**
-     * Check whether POP3 UID is compatible with RFC1939
-     */
-    public static boolean isRFC1939Compatible(String uid) {
-        if (uid == null) {
-            return false;
-        }
-
-        return IntStream.range(0, uid.length())
-            .allMatch(i -> uid.charAt(i) >= 0x21 && uid.charAt(i) <= 0x7E);
-    }
-}
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArguments.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArguments.java
new file mode 100644
index 0000000..35e2964
--- /dev/null
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArguments.java
@@ -0,0 +1,74 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.protocols.api.Request;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
+public class POP3MessageCommandArguments {
+    
+    private final int messageNumber;
+    private final Optional<Integer> lineCount;
+
+    public static Optional<POP3MessageCommandArguments> fromRequest(Request request) {
+        String parameters = request.getArgument();
+        if (parameters == null) {
+            return Optional.empty();
+        }
+
+        try {
+            List<Integer> args = Splitter.on(' ')
+                .omitEmptyStrings()
+                .trimResults()
+                .splitToList(parameters)
+                .stream()
+                .map(Integer::parseInt)
+                .collect(ImmutableList.toImmutableList());
+            
+            if (args.size() == 2) {
+                return Optional.of(new POP3MessageCommandArguments(args.get(0), Optional.of(args.get(1))));
+            } else if (args.size() == 1) {
+                return Optional.of(new POP3MessageCommandArguments(args.get(0), Optional.empty()));
+            } else {
+                return Optional.empty();
+            }
+        } catch (NumberFormatException nfe) {
+            return Optional.empty();
+        }
+    }
+    
+    private POP3MessageCommandArguments(int messsageNumber, Optional<Integer> lineCount) {
+        this.messageNumber = messsageNumber;
+        this.lineCount = lineCount;
+    }
+    
+    int getMessageNumber() {
+        return messageNumber; 
+    }
+    
+    Optional<Integer> getLineCount() {
+        return lineCount;
+    }
+}
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegate.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegate.java
new file mode 100644
index 0000000..ec2f84e
--- /dev/null
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegate.java
@@ -0,0 +1,87 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public abstract class POP3MessageCommandDelegate {
+
+    private final String command;
+    
+    public POP3MessageCommandDelegate(Collection<String> commands) {
+        Preconditions.checkArgument(commands != null && !commands.isEmpty(), "You must provide at least one command keyword");
+        this.command = commands.iterator().next();
+    }
+
+    public Response handleMessageRequest(POP3Session session, Request request) {
+        if (session.getHandlerState() == POP3Session.TRANSACTION) {
+
+            return POP3MessageCommandArguments.fromRequest(request)
+                .map(args -> handleValidArgs(session, args))
+                .orElseGet(this::handleSyntaxError);
+            
+        } else {
+            return POP3Response.ERR;
+        }
+    }
+
+    private Response handleValidArgs(POP3Session session, POP3MessageCommandArguments args) {
+        try {
+
+            MessageMetaData data = getMetaData(session, args.getMessageNumber());
+
+            List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction).orElse(ImmutableList.of());
+            if (!deletedUidList.contains(data.getUid())) {
+                return handleMessageExists(session, data, args);
+            } else {
+                return new POP3Response(POP3Response.ERR_RESPONSE, "Message (" + args.getMessageNumber() + ") already deleted.");
+            }
+
+        } catch (IOException ioe) {
+            return new POP3Response(POP3Response.ERR_RESPONSE, "Message (" + args.getMessageNumber() + ") does not exist.");
+        }
+    }
+
+    private MessageMetaData getMetaData(POP3Session session, int number) throws IOException {
+        return session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)
+            .filter(uidList -> number <= uidList.size())
+            .map(uidList -> uidList.get(number - 1))
+            .orElseThrow(() -> new IOException("MessageMetaData does not exist for number " + number));
+    }
+
+    protected Response handleSyntaxError() {
+        return new POP3Response(POP3Response.ERR_RESPONSE, "Usage: " + command + " [mail number]");
+    }
+
+    protected abstract Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) throws IOException;
+    
+}
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/RetrCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/RetrCmdHandler.java
index 22f31e4..7ba443d 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/RetrCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/RetrCmdHandler.java
@@ -22,12 +22,10 @@ package org.apache.james.protocols.pop3.core;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
-import java.util.List;
 
 import javax.inject.Inject;
 
 import org.apache.james.metrics.api.MetricFactory;
-import org.apache.james.protocols.api.ProtocolSession.State;
 import org.apache.james.protocols.api.Request;
 import org.apache.james.protocols.api.Response;
 import org.apache.james.protocols.pop3.POP3Response;
@@ -35,28 +33,29 @@ import org.apache.james.protocols.pop3.POP3Session;
 import org.apache.james.protocols.pop3.POP3StreamResponse;
 import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
 import org.apache.james.util.MDCBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 /**
  * Handles RETR command
  */
 public class RetrCmdHandler extends AbstractPOP3CommandHandler {
-    private static final Logger LOGGER = LoggerFactory.getLogger(RetrCmdHandler.class);
     private static final Collection<String> COMMANDS = ImmutableSet.of("RETR");
-    @VisibleForTesting
-    static final Response SYNTAX_ERROR = new POP3Response(POP3Response.ERR_RESPONSE, "Usage: RETR [mail number]").immutable();
-    private static final Response ERROR_MESSAGE_RETRIEVE = new POP3Response(POP3Response.ERR_RESPONSE, "Error while retrieving message.").immutable();
 
     private final MetricFactory metricFactory;
+    private final POP3MessageCommandDelegate commandDelegate;
 
     @Inject
     public RetrCmdHandler(MetricFactory metricFactory) {
         this.metricFactory = metricFactory;
+        this.commandDelegate = new POP3MessageCommandDelegate(COMMANDS) {
+            @Override
+            protected Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) throws IOException {
+                InputStream content = getMessageContent(session, data);
+                InputStream in = new CRLFTerminatedInputStream(new ExtraDotInputStream(content));
+                return new POP3StreamResponse(POP3Response.OK_RESPONSE, "Message follows", in);
+            }
+        };
     }
 
     /**
@@ -72,53 +71,7 @@ public class RetrCmdHandler extends AbstractPOP3CommandHandler {
                     .addToContext(MDCBuilder.ACTION, "RETR")
                     .addToContext(MDCConstants.withSession(session))
                     .addToContext(MDCConstants.forRequest(request)),
-                () -> retr(session, request)));
-    }
-
-    private Response retr(POP3Session session, Request request) {
-        LOGGER.trace("RETR command received");
-        POP3Response response = null;
-        String parameters = request.getArgument();
-        if (session.getHandlerState() == POP3Session.TRANSACTION) {
-            int num = 0;
-            try {
-                num = Integer.parseInt(parameters.trim());
-            } catch (Exception e) {
-                return SYNTAX_ERROR;
-            }
-            try {
-                MessageMetaData data = MessageMetaDataUtils.getMetaData(session, num);
-
-                if (data == null) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                    response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                    return response;
-                }
-                List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction).orElse(ImmutableList.of());
-
-                String uid = data.getUid();
-                if (!deletedUidList.contains(uid)) {
-                    InputStream content = getMessageContent(session, data);
-
-                    if (content != null) {
-                        InputStream in = new CRLFTerminatedInputStream(new ExtraDotInputStream(content));
-                        response = new POP3StreamResponse(POP3Response.OK_RESPONSE, "Message follows", in);
-                        return response;
-                    } else {
-                        StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                        response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                    }
-                } else {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") already deleted.");
-                    response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                }
-            } catch (IOException ioe) {
-                return ERROR_MESSAGE_RETRIEVE;
-            }
-        } else {
-            return POP3Response.ERR;
-        }
-        return response;
+                () -> commandDelegate.handleMessageRequest(session, request)));
     }
     
     protected InputStream getMessageContent(POP3Session session, MessageMetaData data) throws IOException {
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/StatCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/StatCmdHandler.java
index 278b932..bbf2bc4 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/StatCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/StatCmdHandler.java
@@ -19,7 +19,6 @@
 
 package org.apache.james.protocols.pop3.core;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -76,17 +75,14 @@ public class StatCmdHandler extends AbstractPOP3CommandHandler {
             long size = 0;
             int count = 0;
             if (!uidList.isEmpty()) {
-                List<MessageMetaData> validResults = new ArrayList<>();
                 for (MessageMetaData data : uidList) {
                     if (!deletedUidList.contains(data.getUid())) {
                         size += data.getSize();
                         count++;
-                        validResults.add(data);
                     }
                 }
             }
-            StringBuilder responseBuffer = new StringBuilder(32).append(count).append(" ").append(size);
-            return new POP3Response(POP3Response.OK_RESPONSE, responseBuffer.toString());
+            return new POP3Response(POP3Response.OK_RESPONSE, count + " " + size);
 
         } else {
             return POP3Response.ERR;
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/TopCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/TopCmdHandler.java
index e7cd77a..755e0e1 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/TopCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/TopCmdHandler.java
@@ -23,14 +23,11 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
-import java.util.NoSuchElementException;
 import java.util.Set;
 
 import javax.inject.Inject;
 
 import org.apache.james.metrics.api.MetricFactory;
-import org.apache.james.protocols.api.ProtocolSession.State;
 import org.apache.james.protocols.api.Request;
 import org.apache.james.protocols.api.Response;
 import org.apache.james.protocols.pop3.POP3Response;
@@ -38,8 +35,6 @@ import org.apache.james.protocols.pop3.POP3Session;
 import org.apache.james.protocols.pop3.POP3StreamResponse;
 import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
 import org.apache.james.util.MDCBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -47,20 +42,34 @@ import com.google.common.collect.ImmutableSet;
 /**
  * Handles TOP command
  */
-public class TopCmdHandler extends RetrCmdHandler implements CapaCapability {
-    private static final Logger LOGGER = LoggerFactory.getLogger(TopCmdHandler.class);
+public class TopCmdHandler extends AbstractPOP3CommandHandler implements CapaCapability {
     private static final Collection<String> COMMANDS = ImmutableList.of("TOP");
     private static final Set<String> CAPS = ImmutableSet.of("TOP");
     
-    private static final Response SYNTAX_ERROR = new POP3Response(POP3Response.ERR_RESPONSE, "Usage: TOP [mail number] [Line number]").immutable();
-    private static final Response ERROR_MESSAGE_RETR = new POP3Response(POP3Response.ERR_RESPONSE, "Error while retrieving message.").immutable();
+    private static final Response SYNTAX_ERROR = new POP3Response(POP3Response.ERR_RESPONSE, "Usage: TOP [mail number] [line count]").immutable();
 
     private final MetricFactory metricFactory;
+    private final POP3MessageCommandDelegate commandDelegate;
 
     @Inject
     public TopCmdHandler(MetricFactory metricFactory) {
-        super(metricFactory);
         this.metricFactory = metricFactory;
+        this.commandDelegate = new POP3MessageCommandDelegate(COMMANDS) {
+            @Override
+            protected Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) throws IOException {
+                if (args.getLineCount().isEmpty()) {
+                    return handleSyntaxError();
+                }
+                InputStream content = getMessageContent(session, data);
+                InputStream in = new CountingBodyInputStream(new CRLFTerminatedInputStream(new ExtraDotInputStream(content)), args.getLineCount().get());
+                return new POP3StreamResponse(POP3Response.OK_RESPONSE, "Message follows", in);
+            }
+
+            @Override
+            protected Response handleSyntaxError() {
+                return SYNTAX_ERROR;
+            }
+        };
     }
 
     /**
@@ -79,62 +88,11 @@ public class TopCmdHandler extends RetrCmdHandler implements CapaCapability {
                     .addToContext(MDCBuilder.ACTION, "TOP")
                     .addToContext(MDCConstants.withSession(session))
                     .addToContext(MDCConstants.forRequest(request)),
-                () -> top(session, request)));
+                () -> commandDelegate.handleMessageRequest(session, request)));
     }
 
-    private Response top(POP3Session session, Request request) {
-        LOGGER.trace("TOP command received");
-        String parameters = request.getArgument();
-        if (parameters == null) {
-            return SYNTAX_ERROR;
-        }
-
-        String argument = "";
-        String argument1 = "";
-        int pos = parameters.indexOf(" ");
-        if (pos > 0) {
-            argument = parameters.substring(0, pos);
-            argument1 = parameters.substring(pos + 1);
-        }
-
-        if (session.getHandlerState() == POP3Session.TRANSACTION) {
-            int num = 0;
-            int lines = -1;
-            try {
-                num = Integer.parseInt(argument);
-                lines = Integer.parseInt(argument1);
-            } catch (NumberFormatException nfe) {
-                return SYNTAX_ERROR;
-            }
-            try {
-
-                MessageMetaData data = MessageMetaDataUtils.getMetaData(session, num);
-                if (data == null) {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                    return  new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                }
-
-                List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction).orElse(ImmutableList.of());
-
-                String uid = data.getUid();
-                if (!deletedUidList.contains(uid)) {
-
-                    InputStream message = new CountingBodyInputStream(new ExtraDotInputStream(new CRLFTerminatedInputStream(getMessageContent(session, data))), lines);
-                    return new POP3StreamResponse(POP3Response.OK_RESPONSE, "Message follows", message);
-
-                } else {
-                    StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") already deleted.");
-                    return new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                }
-            } catch (IOException ioe) {
-                return ERROR_MESSAGE_RETR;
-            } catch (IndexOutOfBoundsException | NoSuchElementException iob) {
-                StringBuilder exceptionBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                return new POP3Response(POP3Response.ERR_RESPONSE, exceptionBuffer.toString());
-            }
-        } else {
-            return POP3Response.ERR;
-        }
+    protected InputStream getMessageContent(POP3Session session, MessageMetaData data) throws IOException {
+        return session.getUserMailbox().getMessage(data.getUid());
     }
 
     @Override
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/UidlCmdHandler.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/UidlCmdHandler.java
index 36f8524..e9cfe7e 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/UidlCmdHandler.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/core/UidlCmdHandler.java
@@ -50,10 +50,18 @@ public class UidlCmdHandler extends AbstractPOP3CommandHandler implements CapaCa
     private static final Set<String> CAPS = ImmutableSet.of("UIDL");
 
     private final MetricFactory metricFactory;
+    private final POP3MessageCommandDelegate commandDelegate;
 
     @Inject
     public UidlCmdHandler(MetricFactory metricFactory) {
         this.metricFactory = metricFactory;
+        this.commandDelegate = new POP3MessageCommandDelegate(COMMANDS) {
+            @Override
+            protected Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) throws IOException {
+                String identifier = session.getUserMailbox().getIdentifier();
+                return new POP3Response(POP3Response.OK_RESPONSE, args.getMessageNumber() + " " + data.getUid(identifier));
+            }
+        };
     }
 
     /**
@@ -68,65 +76,37 @@ public class UidlCmdHandler extends AbstractPOP3CommandHandler implements CapaCa
                     .addToContext(MDCBuilder.ACTION, "UIDL")
                     .addToContext(MDCConstants.withSession(session))
                     .addToContext(MDCConstants.forRequest(request)),
-                () -> uidl(session, request)));
+                () -> handleMessageRequest(session, request)));
     }
 
-    private Response uidl(POP3Session session, Request request) {
+    private Response handleMessageRequest(POP3Session session, Request request) {
         LOGGER.trace("UIDL command received");
-        POP3Response response = null;
-        String parameters = request.getArgument();
+        if (request.getArgument() != null) {
+            return commandDelegate.handleMessageRequest(session, request);
+        }
+        
         if (session.getHandlerState() == POP3Session.TRANSACTION) {
             List<MessageMetaData> uidList = session.getAttachment(POP3Session.UID_LIST, State.Transaction).orElse(ImmutableList.of());
             List<String> deletedUidList = session.getAttachment(POP3Session.DELETED_UID_LIST, State.Transaction).orElse(ImmutableList.of());
             try {
                 String identifier = session.getUserMailbox().getIdentifier();
-                if (parameters == null) {
-                    response = new POP3Response(POP3Response.OK_RESPONSE, "unique-id listing follows");
-
-                    for (int i = 0; i < uidList.size(); i++) {
-                        MessageMetaData metadata = uidList.get(i);
-                        if (!deletedUidList.contains(metadata.getUid())) {
-                            StringBuilder responseBuffer = new StringBuilder().append(i + 1).append(" ").append(metadata.getUid(identifier));
-                            response.appendLine(responseBuffer.toString());
-                        }
-                    }
+                POP3Response response = new POP3Response(POP3Response.OK_RESPONSE, "unique-id listing follows");
 
-                    response.appendLine(".");
-                } else {
-                    int num = 0;
-                    try {
-                        num = Integer.parseInt(parameters);
-
-                        MessageMetaData metadata = MessageMetaDataUtils.getMetaData(session, num);
-
-                        if (metadata == null) {
-                            StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                            return  new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                        }
-
-                        if (deletedUidList.contains(metadata.getUid()) == false) {
-                            StringBuilder responseBuffer = new StringBuilder(64).append(num).append(" ").append(metadata.getUid(identifier));
-                            response = new POP3Response(POP3Response.OK_RESPONSE, responseBuffer.toString());
-                        } else {
-                            StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") already deleted.");
-                            response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                        }
-                    } catch (IndexOutOfBoundsException npe) {
-                        StringBuilder responseBuffer = new StringBuilder(64).append("Message (").append(num).append(") does not exist.");
-                        response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
-                    } catch (NumberFormatException nfe) {
-                        StringBuilder responseBuffer = new StringBuilder(64).append(parameters).append(" is not a valid number");
-                        response = new POP3Response(POP3Response.ERR_RESPONSE, responseBuffer.toString());
+                for (int i = 0; i < uidList.size(); i++) {
+                    MessageMetaData metadata = uidList.get(i);
+                    if (!deletedUidList.contains(metadata.getUid())) {
+                        response.appendLine((i + 1) + " " + metadata.getUid(identifier));
                     }
                 }
-            } catch (IOException e) {
+
+                response.appendLine(".");
+                return response;
+            } catch (IOException ioe) {
                 return POP3Response.ERR;
             }
-
         } else {
             return POP3Response.ERR;
         }
-        return response;
     }
 
     @Override
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/Mailbox.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/Mailbox.java
index 58d73b9..38b5fbb 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/Mailbox.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/Mailbox.java
@@ -27,9 +27,9 @@ import java.util.List;
  */
 public interface Mailbox {
     /**
-     * Return the full message (headers + body) as {@link InputStream} or
-     * <code>null</code> if no message can be found for the given
-     * <code>uid</code>
+     * Return the full message (headers + body) as {@link InputStream}
+     * for the given <code>uid</code>.
+     * @exception IOException If message can not be found or is inaccessible
      */
     InputStream getMessage(String uid) throws IOException;
 
diff --git a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/MessageMetaData.java b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/MessageMetaData.java
index ab4ad90..6dc463b 100644
--- a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/MessageMetaData.java
+++ b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/mailbox/MessageMetaData.java
@@ -18,13 +18,15 @@
  ****************************************************************/
 package org.apache.james.protocols.pop3.mailbox;
 
-import org.apache.james.protocols.pop3.core.MessageMetaDataUtils;
+import com.google.common.base.CharMatcher;
 
 /**
  * Hold meta data for a message
  */
 public class MessageMetaData {
 
+    private static final CharMatcher IS_RFC1939_COMPATIBLE = CharMatcher.inRange((char) 0x21, (char) 0x7E).precomputed();
+
     private final String uid;
     private final long size;
 
@@ -32,7 +34,7 @@ public class MessageMetaData {
         this.uid = uid;
         this.size = size;
 
-        if (!MessageMetaDataUtils.isRFC1939Compatible(uid)) {
+        if (uid == null || !IS_RFC1939_COMPATIBLE.matchesAllOf(uid)) {
             throw new IllegalArgumentException("UID is not RFC1939 compatible");
         }
     }
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/DeleCmdHandlerTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/DeleCmdHandlerTest.java
new file mode 100644
index 0000000..cdd6072
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/DeleCmdHandlerTest.java
@@ -0,0 +1,115 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+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.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.mailbox.Mailbox;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.junit.jupiter.api.Test;
+
+public class DeleCmdHandlerTest {
+
+    @Test
+    void onCommandDeletesInitialMessage() throws IOException {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.empty());
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getMessage(data.getUid())).thenThrow(new IOException("cannot retrieve message content"));
+
+        Response response = new DeleCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("Message deleted");
+
+        verify(session).setAttachment(eq(POP3Session.DELETED_UID_LIST), 
+            argThat(uidList -> uidList.size() == 1 && uidList.contains("1234")),
+            eq(ProtocolSession.State.Transaction));
+    }
+
+    @Test
+    void onCommandDeletesAdditionalMessage() throws IOException {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+        List<String> uidList = new ArrayList<>();
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(uidList));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getMessage(data.getUid())).thenThrow(new IOException("cannot retrieve message content"));
+
+        Response response = new DeleCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("Message deleted");
+
+        assertThat(uidList).containsOnly("1234");
+        verify(session, never()).setAttachment(eq(POP3Session.DELETED_UID_LIST), anyList(), eq(ProtocolSession.State.Transaction));
+    }
+
+    @Test
+    void onCommandHandlesDeletedMessage() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data.getUid())));
+
+        Response response = new DeleCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("already deleted");
+    }
+
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/ListCmdHandlerTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/ListCmdHandlerTest.java
new file mode 100644
index 0000000..980c792
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/ListCmdHandlerTest.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.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.junit.jupiter.api.Test;
+
+public class ListCmdHandlerTest {
+
+    @Test
+    void onCommandHandlesEmptyMailbox() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(Collections.emptyList()));
+
+        Response response = new ListCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0)).isEqualTo("+OK 0 0");
+        assertThat(result.get(1)).isEqualTo(".");
+    }
+    
+    @Test
+    void onCommandRetrievesAllMessages() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(
+            new MessageMetaData("123", 123), new MessageMetaData("456", 456))));
+
+        Response response = new ListCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(4);
+        assertThat(result.get(0)).isEqualTo("+OK 2 579");
+        assertThat(result.get(1)).isEqualTo("1 123");
+        assertThat(result.get(2)).isEqualTo("2 456");
+        assertThat(result.get(3)).isEqualTo(".");
+    }
+
+    @Test
+    void onCommandExcludesDeletedMessages() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(
+            new MessageMetaData("123", 123), new MessageMetaData("456", 456), new MessageMetaData("789", 789))));
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of("456")));
+
+        Response response = new ListCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(4);
+        assertThat(result.get(0)).isEqualTo("+OK 2 912");
+        assertThat(result.get(1)).isEqualTo("1 123");
+        assertThat(result.get(2)).isEqualTo("3 789");
+        assertThat(result.get(3)).isEqualTo(".");
+    }
+
+    @Test
+    void onCommandListsSingleMessage() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        Response response = new ListCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response.getLines()).containsOnly("+OK 1 567");
+    }
+
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArgumentsTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArgumentsTest.java
new file mode 100644
index 0000000..04918b6
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandArgumentsTest.java
@@ -0,0 +1,119 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.james.protocols.api.Request;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class POP3MessageCommandArgumentsTest {
+
+    @ParameterizedTest
+    @ValueSource(ints = {8, 16, 32, 64, 128, 256})
+    void fromRequestShouldNotThrowOnHexNumberOverflow(int pad) {
+        Request request = mock(Request.class);
+        String overflowedNumber = Collections.nCopies(pad, "\\xff").stream().collect(Collectors.joining());
+        when(request.getArgument()).thenReturn(overflowedNumber);
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isEmpty();
+    }
+
+    @Test
+    void fromRequestShouldNotThrowOnMessageDecNumberOverflow() {
+        Request request = mock(Request.class);
+        String overflowedNumber = Long.toString(Long.MAX_VALUE);
+        when(request.getArgument()).thenReturn(overflowedNumber);
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isEmpty();
+    }
+
+    @Test
+    void fromRequestShouldNotThrowOnNullArgument() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isEmpty();
+    }
+
+    @Test
+    void fromRequestShouldNotThrowOnInvalidMessageNumber() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("not-a-number");
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isEmpty();
+    }
+
+    @Test
+    void fromRequestHandlesSingleArgument() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isPresent();
+        assertThat(args.get().getMessageNumber()).isEqualTo(1);
+        assertThat(args.get().getLineCount().isPresent()).isFalse();
+    }
+
+    @Test
+    void fromRequestHandlesTwoArguments() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1 2");
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isPresent();
+        assertThat(args.get().getMessageNumber()).isEqualTo(1);
+        assertThat(args.get().getLineCount().isPresent()).isTrue();
+        assertThat(args.get().getLineCount().get()).isEqualTo(2);
+    }
+
+    @Test
+    void fromRequestReqjectsMultipleArguments() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1 2 3");
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isEmpty();
+    }
+
+    @Test
+    void fromRequestHandlesExtraWhiteSpaceInArgument() {
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("    1    2    ");
+
+        Optional<POP3MessageCommandArguments> args = POP3MessageCommandArguments.fromRequest(request);
+        assertThat(args).isPresent();
+        assertThat(args.get().getMessageNumber()).isEqualTo(1);
+        assertThat(args.get().getLineCount().isPresent()).isTrue();
+        assertThat(args.get().getLineCount().get()).isEqualTo(2);
+    }
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegateTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegateTest.java
new file mode 100644
index 0000000..5ad6477
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/POP3MessageCommandDelegateTest.java
@@ -0,0 +1,132 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.junit.jupiter.api.Test;
+
+public class POP3MessageCommandDelegateTest {
+
+    private static final Response SUCCESS = new POP3Response(POP3Response.OK_RESPONSE, "test succeeded");
+    private static final MessageMetaData MESSAGE_META_DATA = new MessageMetaData("1234", 567);
+    
+    private POP3MessageCommandDelegate commandDelegate = new POP3MessageCommandDelegate(Set.of("TEST")) {
+        @Override
+        protected Response handleMessageExists(POP3Session session, MessageMetaData data, POP3MessageCommandArguments args) throws IOException {
+            return SUCCESS;
+        }
+    };
+
+    @Test
+    void onCommandIncludesKeywordInSyntaxError() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        Response response = commandDelegate.handleMessageRequest(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).isEqualTo("-ERR Usage: TEST [mail number]");
+    }
+
+    @Test
+    void onCommandFailsInAuthReadyState() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.AUTHENTICATION_READY);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        assertThat(commandDelegate.handleMessageRequest(session, request).getRetCode())
+            .isEqualTo(POP3Response.ERR_RESPONSE);
+    }
+
+    @Test
+    void onCommandFailsInUserSetState() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.AUTHENTICATION_USERSET);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        assertThat(commandDelegate.handleMessageRequest(session, request).getRetCode())
+            .isEqualTo(POP3Response.ERR_RESPONSE);
+    }
+
+    @Test
+    void onCommandHandlesNonExistingMessage() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        Response response = commandDelegate.handleMessageRequest(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("does not exist");
+    }
+
+    @Test
+    void onCommandHandlesDeletedMessage() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = MESSAGE_META_DATA;
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data.getUid())));
+
+        Response response = commandDelegate.handleMessageRequest(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("already deleted");
+    }
+
+    @Test
+    void onCommandHandlesExistingMessage() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(MESSAGE_META_DATA)));
+
+        Response response = commandDelegate.handleMessageRequest(session, request);
+        assertThat(response).isEqualTo(SUCCESS);
+    }
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/RetrCmdHandlerTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/RetrCmdHandlerTest.java
index dd0ebac..f90a726 100644
--- a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/RetrCmdHandlerTest.java
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/RetrCmdHandlerTest.java
@@ -23,42 +23,61 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import java.util.Collections;
-import java.util.stream.Collectors;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Optional;
 
 import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.ProtocolSession;
 import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
 import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.POP3StreamResponse;
+import org.apache.james.protocols.pop3.mailbox.Mailbox;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
 
 class RetrCmdHandlerTest {
 
-    @ParameterizedTest
-    @ValueSource(ints = {8, 16, 32, 64, 128, 256})
-    void onCommandShouldNotThrowOnMessageHexNumberOverflow(int pad) {
+    @Test
+    void onCommandHandlesMissingContent() throws IOException {
         POP3Session session = mock(POP3Session.class);
         when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
 
         Request request = mock(Request.class);
-        String overflowedNumber = Collections.nCopies(pad, "\\xff").stream().collect(Collectors.joining());
-        when(request.getArgument()).thenReturn(overflowedNumber);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getMessage(data.getUid())).thenThrow(new IOException("cannot retrieve message content"));
 
-        assertThat(new RetrCmdHandler(new RecordingMetricFactory()).onCommand(session, request))
-            .isEqualTo(RetrCmdHandler.SYNTAX_ERROR);
+        Response response = new RetrCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("does not exist");
     }
 
     @Test
-    void onCommandShouldNotThrowOnMessageDecNumberOverflow() {
+    void onCommandRetrievesMessage() throws IOException {
         POP3Session session = mock(POP3Session.class);
         when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
 
         Request request = mock(Request.class);
-        String overflowedNumber = Long.toString(Long.MAX_VALUE);
-        when(request.getArgument()).thenReturn(overflowedNumber);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getMessage(data.getUid())).thenReturn(InputStream.nullInputStream());
 
-        assertThat(new RetrCmdHandler(new RecordingMetricFactory()).onCommand(session, request))
-            .isEqualTo(RetrCmdHandler.SYNTAX_ERROR);
+        Response response = new RetrCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3StreamResponse.class);
     }
 }
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/TopCmdHandlerTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/TopCmdHandlerTest.java
new file mode 100644
index 0000000..1d9cc59
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/TopCmdHandlerTest.java
@@ -0,0 +1,125 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.POP3StreamResponse;
+import org.apache.james.protocols.pop3.mailbox.Mailbox;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.junit.jupiter.api.Test;
+
+public class TopCmdHandlerTest {
+
+    @Test
+    void onCommandShowsSpecificSyntaxError() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        Response response = new TopCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).isEqualTo("-ERR Usage: TOP [mail number] [line count]");
+    }
+
+    @Test
+    void onCommandDetectsMissingLinesNumber() {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+        
+        Response response = new TopCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("Usage:");
+    }
+
+    @Test
+    void onCommandHandlesMissingContent() throws IOException {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1 2");
+
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getMessage(data.getUid())).thenThrow(new IOException("cannot retrieve message content"));
+
+        Response response = new TopCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.ERR_RESPONSE);
+        assertThat(response.getLines().get(0)).contains("does not exist");
+    }
+    
+    @Test
+    void onCommandRetrievesMessageLines() throws IOException {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1 2");
+        
+        MessageMetaData data = new MessageMetaData("1234", 567);
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(data)));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        String message = 
+            "Subject: test\r\n" +
+            "\r\n" +
+            "line1\r\n" +
+            "line2\r\n" +
+            "line3\r\n";
+        when(mailbox.getMessage(data.getUid())).thenReturn(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)));
+
+        Response response = new TopCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3StreamResponse.class);
+        
+        String result = IOUtils.toString(((POP3StreamResponse) response).getStream(), StandardCharsets.UTF_8);
+        assertThat(result).contains("line1");
+        assertThat(result).contains("line2");
+        assertThat(result).doesNotContain("line3");
+    }
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/UidlCmdHandlerTest.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/UidlCmdHandlerTest.java
new file mode 100644
index 0000000..8b679cc
--- /dev/null
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/core/UidlCmdHandlerTest.java
@@ -0,0 +1,139 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.pop3.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.ProtocolSession;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.mailbox.Mailbox;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.junit.jupiter.api.Test;
+
+public class UidlCmdHandlerTest {
+
+    @Test
+    void onCommandHandlesEmptyMailbox() throws Exception {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(Collections.emptyList()));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getIdentifier()).thenReturn("id");
+        
+        Response response = new UidlCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0)).startsWith("+OK ");
+        assertThat(result.get(1)).isEqualTo(".");
+    }
+    
+    @Test
+    void onCommandRetrievesAllUids() throws Exception {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(
+            new MessageMetaData("123", 123), new MessageMetaData("456", 456))));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getIdentifier()).thenReturn("id");
+        
+        Response response = new UidlCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(4);
+        assertThat(result.get(0)).startsWith("+OK ");
+        assertThat(result.get(1)).isEqualTo("1 123");
+        assertThat(result.get(2)).isEqualTo("2 456");
+        assertThat(result.get(3)).isEqualTo(".");
+    }
+
+    @Test
+    void onCommandExcludesDeletedUids() throws Exception {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn(null);
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(
+            new MessageMetaData("123", 123), new MessageMetaData("456", 456))));
+        when(session.getAttachment(POP3Session.DELETED_UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of("123")));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getIdentifier()).thenReturn("id");
+        
+        Response response = new UidlCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response).isInstanceOf(POP3Response.class);
+
+        List<CharSequence> result = response.getLines();
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0)).startsWith("+OK ");
+        assertThat(result.get(1)).isEqualTo("2 456");
+        assertThat(result.get(2)).isEqualTo(".");
+    }
+
+    @Test
+    void onCommandRetrievesSingleUid() throws Exception {
+        POP3Session session = mock(POP3Session.class);
+        when(session.getHandlerState()).thenReturn(POP3Session.TRANSACTION);
+
+        Request request = mock(Request.class);
+        when(request.getArgument()).thenReturn("1");
+
+        when(session.getAttachment(POP3Session.UID_LIST, ProtocolSession.State.Transaction)).thenReturn(Optional.of(List.of(
+            new MessageMetaData("1234", 567))));
+
+        Mailbox mailbox = mock(Mailbox.class);
+        when(session.getUserMailbox()).thenReturn(mailbox);
+        when(mailbox.getIdentifier()).thenReturn("id");
+        
+        Response response = new UidlCmdHandler(new RecordingMetricFactory()).onCommand(session, request);
+        assertThat(response.getRetCode()).isEqualTo(POP3Response.OK_RESPONSE);
+        assertThat(response.getLines()).containsOnly("+OK 1 1234");
+    }
+}
diff --git a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/utils/MockMailbox.java b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/utils/MockMailbox.java
index 0605ac4..e9b96e6 100644
--- a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/utils/MockMailbox.java
+++ b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/utils/MockMailbox.java
@@ -19,6 +19,7 @@
 package org.apache.james.protocols.pop3.utils;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.SequenceInputStream;
 import java.nio.charset.StandardCharsets;
@@ -69,11 +70,11 @@ public class MockMailbox extends ImapMailbox {
     }
 
     @Override
-    public InputStream getMessage(long uid) {
+    public InputStream getMessage(long uid) throws IOException {
         InputStream body = getMessageBody(uid);
         InputStream headers = getMessageHeaders(uid);
         if (body == null || headers == null) {
-            return null;
+            throw new IOException("Message does not exist for uid " + uid);
         }
         return new SequenceInputStream(headers, body);
     }
diff --git a/server/protocols/protocols-pop3-distributed/src/main/java/org/apache/james/pop3server/mailbox/DistributedMailboxAdapter.java b/server/protocols/protocols-pop3-distributed/src/main/java/org/apache/james/pop3server/mailbox/DistributedMailboxAdapter.java
index b6869b9..b86774b 100644
--- a/server/protocols/protocols-pop3-distributed/src/main/java/org/apache/james/pop3server/mailbox/DistributedMailboxAdapter.java
+++ b/server/protocols/protocols-pop3-distributed/src/main/java/org/apache/james/pop3server/mailbox/DistributedMailboxAdapter.java
@@ -107,7 +107,7 @@ public class DistributedMailboxAdapter implements Mailbox {
                 LOGGER.warn("Removing {} from {} POP3 projection for user {} at it is not backed by a MailboxMessage",
                     uid, mailbox.getId().serialize(), session.getUser().asString());
                 Mono.from(metadataStore.remove(mailbox.getId(), messageId)).block();
-                return null;
+                throw new IOException("Message does not exist for uid " + uid);
             }
         } catch (MailboxException e) {
             throw new IOException("Unable to retrieve message body for uid " + uid, e);
diff --git a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/mailbox/MailboxAdapter.java b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/mailbox/MailboxAdapter.java
index 9263970..52ec591 100644
--- a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/mailbox/MailboxAdapter.java
+++ b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/mailbox/MailboxAdapter.java
@@ -62,7 +62,7 @@ public class MailboxAdapter implements Mailbox {
             if (results.hasNext()) {
                 return results.next().getFullContent().getInputStream();
             } else {
-                return null;
+                throw new IOException("Message does not exist for uid " + uid);
             }
         } catch (MailboxException e) {
             throw new IOException("Unable to retrieve message for uid " + uid, e);
diff --git a/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java b/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
index 4302476..128450a 100644
--- a/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
+++ b/server/protocols/protocols-pop3/src/test/java/org/apache/james/pop3server/POP3ServerTest.java
@@ -549,11 +549,6 @@ public class POP3ServerTest {
         mailboxManager.deleteMailbox(mailboxPath, session);
     }
 
-    @Disabled("JAMES-3709 Concurrent deletes causes NPE when retrieving messages" +
-        "POP3: UIDL" +
-        "Delete the messages" +
-        "POP3: RETR XXX" +
-        "   /!\\ NPE")
     @Test
     void pop3SessionShouldTolerateConcurrentDeletes() throws Exception {
         finishSetUp(pop3Configuration);
@@ -591,9 +586,9 @@ public class POP3ServerTest {
 
         Reader r = pop3Client.retrieveMessageTop(entries[0].number, 0);
 
-        assertThat(r).isNull();
+        assertThat(r).isNull(); // = message not found
 
-        // Fails: The NPE is not handled and causes the connection to abort...
+        // POP3 session must still be valid afterwards
         assertThatCode(() -> pop3Client.listMessages()).doesNotThrowAnyException();
     }
 

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