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

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


The following commit(s) were added to refs/heads/master by this push:
     new 72b9500d35 [FIX] IMAP: Examine should honor its read-only promises
72b9500d35 is described below

commit 72b9500d359f96dcbad23925401764aaf37a496e
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Jul 27 23:33:08 2026 +0200

    [FIX] IMAP: Examine should honor its read-only promises
---
 .../mpt/imapmailbox/suite/AuthenticatedState.java  |   5 +
 .../apache/james/imap/scripts/ExamineReadOnly.test | 124 +++++++++++++++++++++
 .../james/imap/api/process/SelectedMailbox.java    |  18 +++
 .../imap/processor/AbstractSelectionProcessor.java |   1 +
 .../james/imap/processor/CloseProcessor.java       |   7 ++
 .../james/imap/processor/ExpungeProcessor.java     |   5 +-
 .../james/imap/processor/StoreProcessor.java       |   6 +
 .../imap/processor/base/SelectedMailboxImpl.java   |  11 ++
 .../imap/processor/fetch/FetchResponseBuilder.java |  11 +-
 9 files changed, 184 insertions(+), 4 deletions(-)

diff --git 
a/mpt/impl/imap-mailbox/core/src/main/java/org/apache/james/mpt/imapmailbox/suite/AuthenticatedState.java
 
b/mpt/impl/imap-mailbox/core/src/main/java/org/apache/james/mpt/imapmailbox/suite/AuthenticatedState.java
index 526a105b2e..8170fb584d 100644
--- 
a/mpt/impl/imap-mailbox/core/src/main/java/org/apache/james/mpt/imapmailbox/suite/AuthenticatedState.java
+++ 
b/mpt/impl/imap-mailbox/core/src/main/java/org/apache/james/mpt/imapmailbox/suite/AuthenticatedState.java
@@ -74,6 +74,11 @@ public abstract class AuthenticatedState extends 
BasicImapCommands {
         simpleScriptedTestProtocol.run("AppendExamineInbox");
     }
 
+    @Test
+    public void testExamineReadOnlyUS() throws Exception {
+        simpleScriptedTestProtocol.run("ExamineReadOnly");
+    }
+
     @Test
     public void testAppendSelectInboxUS() throws Exception {
         simpleScriptedTestProtocol.run("AppendSelectInbox");
diff --git 
a/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ExamineReadOnly.test
 
b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ExamineReadOnly.test
new file mode 100644
index 0000000000..dbae90f282
--- /dev/null
+++ 
b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ExamineReadOnly.test
@@ -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.                                           #
+################################################################
+# A mailbox selected by EXAMINE is read-only: no change to its permanent 
state, including
+# per-user state, is permitted.
+#
+# https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2
+C: A003 APPEND inbox {254+}
+C: Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
+C: From: Fred Foobar <[email protected]>
+C: Subject: Test 01
+C: To: [email protected]
+C: Message-Id: <[email protected]>
+C: MIME-Version: 1.0
+C: Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
+C:
+C: Test 01
+C:
+S: A003 OK (\[.+\] )?APPEND completed.
+
+C: A003 APPEND inbox {254+}
+C: Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
+C: From: Fred Foobar <[email protected]>
+C: Subject: Test 02
+C: To: [email protected]
+C: Message-Id: <[email protected]>
+C: MIME-Version: 1.0
+C: Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
+C:
+C: Test 02
+C:
+S: A003 OK (\[.+\] )?APPEND completed.
+
+# Mark the first message \Deleted while the mailbox is still writeable, so 
that we can later
+# assert that CLOSE on a read-only mailbox does not expunge it.
+C: rw1 SELECT inbox
+S: \* OK \[MAILBOXID \(.+\)\] Ok
+S: \* FLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\Seen\)
+S: \* 2 EXISTS
+S: \* 2 RECENT
+S: \* OK \[UIDVALIDITY \d+\].*
+S: \* OK \[UNSEEN 1\].*
+S: \* OK \[PERMANENTFLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\\Seen( 
\\\*)?\)\].*
+S: \* OK \[HIGHESTMODSEQ \d+\].*
+S: \* OK \[UIDNEXT 3\].*
+S: rw1 OK \[READ-WRITE\] SELECT completed.
+
+C: rw2 STORE 1 +FLAGS.SILENT (\Deleted)
+S: rw2 OK STORE completed.
+
+# Re-selecting the very same mailbox with EXAMINE must turn the selection 
read-only.
+C: ro1 EXAMINE inbox
+S: \* OK \[MAILBOXID \(.+\)\] Ok
+S: \* FLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\Seen\)
+S: \* 2 EXISTS
+S: \* 2 RECENT
+S: \* OK \[UIDVALIDITY \d+\].*
+S: \* OK \[UNSEEN 1\].*
+S: \* OK \[PERMANENTFLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\\Seen( 
\\\*)?\)\].*
+S: \* OK \[HIGHESTMODSEQ \d+\].*
+S: \* OK \[UIDNEXT 3\].*
+S: ro1 OK \[READ-ONLY\] EXAMINE completed.
+
+# A non peeking BODY[] implicitly sets \Seen: not here. No FLAGS response 
either, as no flag changed.
+C: ro2 FETCH 2 (BODY[])
+S: \* 2 FETCH \(BODY\[\] \{254\}
+S: Date: Mon, 7 Feb 1994 21:52:25 -0800 \(PST\)
+S: From: Fred Foobar <foobar@Blurdybloop\.COM>
+S: Subject: Test 02
+S: To: mooch@owatagu\.siam\.edu
+S: Message-Id: <B27397-0100000@Blurdybloop\.COM>
+S: MIME-Version: 1\.0
+S: Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
+S:
+S: Test 02
+S: \)
+S: ro2 OK FETCH completed.
+
+C: ro3 FETCH 2 (FLAGS)
+S: \* 2 FETCH \(FLAGS \(\\Recent\)\)
+S: ro3 OK FETCH completed.
+
+C: ro4 STORE 2 +FLAGS (\Flagged)
+S: ro4 NO STORE failed. Mailbox is read only.
+
+C: ro5 EXPUNGE
+S: ro5 NO EXPUNGE failed. Mailbox is read only.
+
+# CLOSE removes no message and gives no error when the mailbox is selected 
read-only.
+C: ro6 CLOSE
+S: ro6 OK CLOSE completed.
+
+# Nothing was persisted: the \Deleted message is still there, the fetched one 
is still unseen.
+C: rw3 SELECT inbox
+S: \* OK \[MAILBOXID \(.+\)\] Ok
+S: \* FLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\Seen\)
+S: \* 2 EXISTS
+S: \* 0 RECENT
+S: \* OK \[UIDVALIDITY \d+\].*
+S: \* OK \[UNSEEN 1\].*
+S: \* OK \[PERMANENTFLAGS \(\\Answered \\Deleted \\Draft \\Flagged \\\Seen( 
\\\*)?\)\].*
+S: \* OK \[HIGHESTMODSEQ \d+\].*
+S: \* OK \[UIDNEXT 3\].*
+S: rw3 OK \[READ-WRITE\] SELECT completed.
+
+C: rw4 FETCH 1:2 (FLAGS)
+S: \* 1 FETCH \(FLAGS \(\\Deleted\)\)
+S: \* 2 FETCH \(FLAGS \(\)\)
+S: rw4 OK FETCH completed.
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java
index 241c27c9c5..bc8b8e7415 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java
@@ -179,6 +179,24 @@ public interface SelectedMailbox {
 
     List<MessageUid> allUids();
     
+    /**
+     * Was this mailbox selected read-only, that is to say by EXAMINE rather 
than by SELECT ?
+     *
+     * https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2
+     *      [EXAMINE] is identical to SELECT and returns the same output; 
however, the selected
+     *      mailbox is identified as read-only. No changes to the permanent 
state of the mailbox,
+     *      including per-user state, are permitted.
+     *
+     * @return true when the mailbox was selected by EXAMINE
+     */
+    boolean isReadOnly();
+
+    /**
+     * Set whether this mailbox is selected read-only. A mailbox already 
selected by SELECT can be
+     * re-selected by EXAMINE - and the other way around - within the same 
session.
+     */
+    void setReadOnly(boolean readOnly);
+
     /**
      * Return all applicable Flags for the selected mailbox
      */
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractSelectionProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractSelectionProcessor.java
index 520f083659..6b66ead2e4 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractSelectionProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractSelectionProcessor.java
@@ -422,6 +422,7 @@ abstract class AbstractSelectionProcessor<R extends 
AbstractMailboxSelectionRequ
                 }
             }))
             .flatMap(Throwing.function(mailbox -> selectMailbox(session, 
responder, mailbox, currentMailbox)
+                .doOnNext(sessionMailbox -> 
sessionMailbox.setReadOnly(openReadOnly))
                 .flatMap(Throwing.function(sessionMailbox ->
                     mailbox.getMetaDataReactive(recentMode(!openReadOnly, 
mailbox, mailboxSession), mailboxSession, 
EnumSet.of(MailboxMetaData.Item.FirstUnseen, 
MailboxMetaData.Item.HighestModSeq, MailboxMetaData.Item.NextUid, 
MailboxMetaData.Item.MailboxCounters))
                         .doOnNext(next -> addRecent(next, sessionMailbox))))));
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/CloseProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/CloseProcessor.java
index 65a7538a62..222df67bf5 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/CloseProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/CloseProcessor.java
@@ -57,6 +57,13 @@ public class CloseProcessor extends 
AbstractMailboxProcessor<CloseRequest> {
         MailboxSession mailboxSession = session.getMailboxSession();
         return getSelectedMailboxReactive(session)
             .flatMap(Throwing.function(mailbox -> {
+                // https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.2
+                //      no messages are removed, and no error is given, if the 
mailbox is selected by an
+                //      EXAMINE command or is otherwise selected read-only.
+                if (session.getSelected().isReadOnly()) {
+                    return session.deselect()
+                        .then(Mono.fromRunnable(() -> okComplete(request, 
responder)));
+                }
                 if (getMailboxManager().hasRight(mailbox.getMailboxEntity(), 
MailboxACL.Right.PerformExpunge, mailboxSession)) {
                     return mailbox.expungeReactive(MessageRange.all(), 
mailboxSession)
                         .count()
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/ExpungeProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/ExpungeProcessor.java
index 9c302e9bda..7aa9a9c79e 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/ExpungeProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/ExpungeProcessor.java
@@ -74,7 +74,10 @@ public class ExpungeProcessor extends 
AbstractMailboxProcessor<ExpungeRequest> i
 
         return getSelectedMailboxReactive(session)
             .flatMap(Throwing.function(mailbox -> {
-                if (!getMailboxManager().hasRight(mailbox.getMailboxEntity(), 
MailboxACL.Right.PerformExpunge, mailboxSession)) {
+                // https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2
+                //      No changes to the permanent state of the mailbox, 
including per-user state, are permitted.
+                if (session.getSelected().isReadOnly()
+                    || 
!getMailboxManager().hasRight(mailbox.getMailboxEntity(), 
MailboxACL.Right.PerformExpunge, mailboxSession)) {
                     no(request, responder, 
HumanReadableText.MAILBOX_IS_READ_ONLY);
                     return Mono.empty();
                 } else {
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/StoreProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/StoreProcessor.java
index 89ed89190e..dc522c9c69 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/StoreProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/StoreProcessor.java
@@ -91,6 +91,12 @@ public class StoreProcessor extends 
AbstractMailboxProcessor<StoreRequest> {
         }
 
         SelectedMailbox selected = session.getSelected();
+        if (selected.isReadOnly()) {
+            // https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2
+            //      No changes to the permanent state of the mailbox, 
including per-user state, are permitted.
+            no(request, responder, HumanReadableText.MAILBOX_IS_READ_ONLY);
+            return Mono.empty();
+        }
         MailboxSession mailboxSession = session.getMailboxSession();
 
         return getSelectedMailboxReactive(session)
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java
index 6a94411c74..1698be2dba 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java
@@ -136,6 +136,7 @@ public class SelectedMailboxImpl implements 
SelectedMailbox, EventListener.React
     private final AtomicBoolean isDeletedByOtherSession = new 
AtomicBoolean(false);
     private final AtomicBoolean sizeChanged = new AtomicBoolean(false);
     private final AtomicBoolean silentFlagChanges = new AtomicBoolean(false);
+    private final AtomicBoolean readOnly = new AtomicBoolean(false);
     private ApplicableFlags applicableFlags = ApplicableFlags.from(new 
Flags());
 
     public SelectedMailboxImpl(MailboxManager mailboxManager, EventBus 
eventBus, MailboxSession session, MessageManager messageManager) {
@@ -219,6 +220,16 @@ public class SelectedMailboxImpl implements 
SelectedMailbox, EventListener.React
         return messageManager;
     }
 
+    @Override
+    public boolean isReadOnly() {
+        return readOnly.get();
+    }
+
+    @Override
+    public void setReadOnly(boolean readOnly) {
+        this.readOnly.set(readOnly);
+    }
+
     @Override
     public synchronized boolean removeRecent(MessageUid uid) {
         final boolean result = recentUids.remove(uid);
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
index 6b0f51c542..d47d34de61 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchResponseBuilder.java
@@ -229,7 +229,7 @@ public final class FetchResponseBuilder {
     }
 
     private Mono<Void> addFlags(FetchData fetch, MessageManager mailbox, 
SelectedMailbox selected, MessageUid resultUid, MailboxSession mailboxSession, 
Flags flags) {
-        return ensureFlagResponse(fetch, mailbox, resultUid, mailboxSession, 
flags)
+        return ensureFlagResponse(fetch, mailbox, selected, resultUid, 
mailboxSession, flags)
             .doOnNext(ensureFlagsResponse -> {
                 if (fetch.contains(Item.FLAGS) || ensureFlagsResponse) {
                     if (selected.isRecent(resultUid)) {
@@ -241,8 +241,13 @@ public final class FetchResponseBuilder {
             .then();
     }
 
-    private Mono<Boolean> ensureFlagResponse(FetchData fetch, MessageManager 
mailbox, MessageUid resultUid, MailboxSession mailboxSession, Flags flags) {
-        if (fetch.isSetSeen() && !flags.contains(Flags.Flag.SEEN)) {
+    private Mono<Boolean> ensureFlagResponse(FetchData fetch, MessageManager 
mailbox, SelectedMailbox selected, MessageUid resultUid, MailboxSession 
mailboxSession, Flags flags) {
+        // A BODY[] - as opposed to a BODY.PEEK[] - implicitly sets \Seen, 
which a mailbox selected by
+        // EXAMINE does not allow.
+        //
+        // https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2
+        //      No changes to the permanent state of the mailbox, including 
per-user state, are permitted.
+        if (fetch.isSetSeen() && !selected.isReadOnly() && 
!flags.contains(Flags.Flag.SEEN)) {
             return Mono.from(mailbox.setFlagsReactive(new 
Flags(Flags.Flag.SEEN), MessageManager.FlagsUpdateMode.ADD, 
MessageRange.one(resultUid), mailboxSession))
                 .then(Mono.fromCallable(() -> {
                     flags.add(Flags.Flag.SEEN);


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

Reply via email to