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]