Skip to content

fix: protocol bugs found in the documentation review - #95

Merged
andris9 merged 19 commits into
masterfrom
fix/review-bugs
Oct 8, 2026
Merged

andris9 merged 19 commits into
masterfrom
fix/review-bugs

Conversation

@andris9

@andris9 andris9 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes the server bugs found while writing and verifying the imapkit.com docs. Each fix was checked against the RFC text and has tests.

  • Notifications: new-message EXISTS and flag updates go out before FETCH, STORE and SEARCH responses (RFC 3501 5.2, 7.4.1), UID SEARCH ends with OK [EXPUNGEISSUED] like SEARCH, and IMAP4rev1 sessions get RECENT after new messages (RFC 3501 7.3.2).
  • LIST: patterns match full names with a prefixed personal namespace such as Cyrus INBOX. (RFC 3501 6.3.8), and # is a break-out character that overrides the reference.
  • Strictness: CREATE ... (USE (NIL)) is BAD instead of a SERVERBUG crash (RFC 6154), and AUTHENTICATE after login is BAD whatever the mechanism. Storage internal dates must be RFC 3501 date-time values.
  • Flags: STORE, APPEND and COPY accept exactly what PERMANENTFLAGS advertises (RFC 3501 7.1).
  • ENVELOPE: an obsolete source route goes to addr-adl, the same as Dovecot (RFC 9051 7.5.2).
  • Plugins: METADATA and METADATA-SERVER load ENABLE (RFC 5464 4.1), quirks that would remove a plugin IMAP4rev2 requires now throw, and xoauth2.sessionTimeout is deprecated because it was never enforced.

The docs and README are updated to match. /simplify and /security-review ran over the branch; the security review found nothing.

🤖 Generated with Claude Code

andris9 and others added 19 commits October 8, 2026 19:27
In a mailbox with allowPermanentFlags false, SELECT listed the flags its
messages have or had in PERMANENTFLAGS, but STORE and the control API
checked only the permanentFlags of the mailbox and dropped the others.
Both now use server.isPermanentFlag(), the same list SELECT sends, and
APPEND and COPY leave out flags the mailbox can not store instead of
turning them into new keywords (RFC 3501 section 7.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty LIST reference means the mailbox name is interpreted as by
SELECT (RFC 3501 section 6.3.8, RFC 9051 section 6.3.9), so LIST "" "INBOX.%"
lists the children of INBOX in a Cyrus style "INBOX." namespace and
LIST "" "%" lists INBOX. Namespaces other than the personal one are still
hidden from wildcards unless the pattern names their prefix. LIST with an
empty mailbox name returns the delimiter and root of the reference
namespace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 3516 and RFC 9051 section 6.4.5 define no error for a part number
that does not exist, and RFC 3516 section 4.2 gives BINARY the semantics
of BODY, so the empty string ImapKit (and Dovecot) send stays. The code
comments no longer claim that RFC 3501 section 6.4.5 requires it, and
the BINARY docs describe the behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…RCH responses

RFC 3501 section 7.4.1 forbids only EXPUNGE during FETCH, STORE and SEARCH,
and section 5.2 requires mailbox size updates during a command. Commands that
refer to messages now first send the EXISTS and flag updates of other sessions
that were queued before any pending EXPUNGE, so a session never gets a FETCH
response for a message it was not told about. What was queued after a pending
EXPUNGE still waits for it, as do the sequence number forms of COPY and MOVE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…UNGE

UID SEARCH with message numbers in the criteria keeps the EXPUNGE responses
of other sessions back like SEARCH does, but ended with a plain OK. It now
carries EXPUNGEISSUED too (RFC 5530 section 3, RFC 9051 section 7.1), as do
UID SORT and UID THREAD with message numbers in the criteria.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…attr

CREATE W (USE (NIL)) crashed with NO [SERVERBUG]. RFC 6154 section 6
defines use-attr-ext = "\" atom, so NIL, quoted strings, literals, numbers
and atoms without a backslash are BAD, and unsupported attributes keep
getting NO [USEATTR] (section 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For <@route.example:a@b.c> the route leaked into the mailbox name of the
ENVELOPE address. RFC 9051 section 7.5.2 makes the second field the
at-domain-list (the obs-route of RFC 5322 section 4.4) and the third the
local-part, so the route now goes to addr-adl as "@route.example", the
way Dovecot sends it (golden form in test/fixtures/mime/source-route.eml).
SORT FROM/TO/CC use the local part too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sessionTimeout came from hoodiecrow and was stored and defaulted to an
hour but never used, access tokens do not expire. Enforcing it would make
the default token of a long running server stop working after an hour,
so it stays accepted and is marked deprecated in the types and docs,
which now show how to test an expired token with control.updateUser().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After login, AUTHENTICATE with an unknown mechanism got NO while a known
one got BAD. AUTHENTICATE is only valid in the not authenticated state
(RFC 3501 section 6.2), so both are now BAD with the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 5464 section 4.1: a server that sends unsolicited METADATA responses
MUST support the ENABLE command, and sends them only after ENABLE
METADATA (or METADATA-SERVER). Without the ENABLE plugin a client could
not turn them on, so both plugins now list ENABLE in plugin.requires,
like UTF8=ACCEPT and UIDONLY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lues

A storage message with an internaldate like "Thu, 1 Jan 2026 10:00:00 +0000"
was accepted, FETCH then sent an invalid INTERNALDATE and SORT ARRIVAL fell
back. validateStorage now requires a date-time string of a real date and
time (RFC 3501 section 9) or a valid Date for internaldate and SAVEDATE
(RFC 8514 section 4.2), so the server fails when it is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 3501 section 7.3.2: the RECENT response occurs "if the size of the
mailbox changes (e.g., new messages)" and the client MUST record it. A run
of EXISTS responses that announces new messages (from another session, the
session's own APPEND, COPY or MOVE, SMTP or the control API) is now followed
by one RECENT response with the number of \Recent messages the session
knows. With NOTIFY it follows the FETCH of the new message (RFC 5465 section
5.2). Sessions that enabled IMAP4rev2 do not get it (RFC 9051 Appendix E).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
no-move and no-uidplus removed MOVE and UIDPLUS even when IMAP4rev2 was
loaded, which advertised IMAP4rev2 without the MOVE command, UID EXPUNGE,
APPENDUID and COPYUID that RFC 9051 folds into the base protocol
(Appendix E). The server constructor now throws "IMAP4rev2 requires MOVE,
which the "no-move" quirk removes" when a loaded plugin requires a plugin
a quirk removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…essions

Real transcripts for new messages reported before FETCH responses, a new
message that waits for a pending EXPUNGE, UID SEARCH with OK [EXPUNGEISSUED]
and the RECENT response after EXISTS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 3501 section 6.3.8 and RFC 9051 section 6.3.9: with the namespace
convention "#" is a break out character "and must be treated as such",
so LIST "Work/" "#news.*" lists the #news. namespace instead of looking
for "Work/#news.*". CLAUDE.md describes the new notification timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unknown AUTHENTICATE mechanisms run through the command queue with a
  fallback handler, so the central checks (state, script rules,
  commandChecks) apply as for every command
- Reuse isAtom for EXISTS, EXPUNGE and CREATE-SPECIAL-USE checks, the
  ATOM-CHAR grammar of imap-handler for use-attr, and getNamespace for
  LIST with an empty mailbox name
- One RECENT helper that counts the session's \Recent set, one snapshot
  lookup per flush, the sequence number check cached per command, one
  pattern per namespace in matchFolders
- One permanent flag list for PERMANENTFLAGS and STORE, one storage
  date check, one user credentials type, the quirk requires check inside
  the plugin loader

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andris9
andris9 merged commit a986f7b into master Oct 8, 2026
9 checks passed
@andris9
andris9 deleted the fix/review-bugs branch October 8, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant