Skip to main content

Strict by design

ImapKit is a guardrail for developing standards compliant IMAP clients. It follows the RFCs strictly instead of accepting whatever a client sends.

Most production servers are lenient. They guess what a sloppy command meant, accept literals before the continuation request, or ignore extra arguments. A client that works against them can still break against the next, stricter server, and the bug shows up in production. ImapKit answers such input with BAD (or NO where the RFC asks for it), so the bug shows up in your test suite instead.

Reading a BAD​

When your client gets BAD from ImapKit, the client broke the protocol grammar or a MUST of an RFC. The human readable text says what was wrong:

C: A1 SELECT INBOX
S: A1 BAD SELECT is not allowed in the Not Authenticated state
C: A2 LOGIN testuser testpass
S: A2 OK User logged in
C: A3 NOOP now
S: A3 BAD NOOP does not take any arguments
C: A4 SELECT INBOX
S: * 1 EXISTS
S: A4 OK [READ-WRITE] Completed
C: A5 FETCH 2 FLAGS
S: A5 BAD Message sequence number 2 is greater than the number of messages (1)
C: A6 STORE 1 +FLAGS (\Recent)
S: A6 BAD Invalid system flag \Recent
C: A7 SEARCH SUBJECT "café"
S: * BAD [SYNTAX] Unexpected char at position 22
S: A7 BAD Error parsing command
C: A8 CREATE "Café"
S: * BAD [SYNTAX] Unexpected char at position 14
S: A8 BAD Error parsing command
C: A9 CREATE "Caf&AOk-"
S: A9 OK CREATE completed

Some untagged SELECT responses are left out above. A7 needs CHARSET UTF-8 for an 8-bit search string, and A8 must encode the mailbox name in modified UTF-7, as A9 does.

NO is different: it means the command was valid but could not be carried out (a missing mailbox, a wrong password, an expunged message). Many NO responses carry a response code that tells the client what to do next, see Response codes.

Some RFC 2683 recommendations that a client SHOULD follow are flagged without failing the command: STATUS on the selected mailbox completes, but with OK [CLIENTBUG].

C: B1 STATUS INBOX (MESSAGES)
S: * STATUS INBOX (MESSAGES 1)
S: B1 OK [CLIENTBUG] Status completed, STATUS SHOULD NOT be used on the selected mailbox
C: B2 EXAMINE INBOX
S: * 1 EXISTS
S: B2 OK [READ-ONLY] Completed
C: B3 STORE 1 +FLAGS (\Seen)
S: B3 NO [CLIENTBUG] Mailbox is read-only

A simple rule for client tests: fail the test on any tagged BAD, and on any CLIENTBUG code.

The rules​

ImapKit answers these with BAD, or NO where noted. The list matches the README and is covered by test/conformance.test.ts and the tests of each plugin.

Commands and arguments​

RuleReference
Commands sent in the wrong state, for example FETCH before SELECT or LOGIN and AUTHENTICATE (with any mechanism) after loginRFC 3501 section 3
Arguments to commands that take none (NOOP x, CLOSE x), missing or extra arguments, and values that break the grammarRFC 3501 section 9
Invalid sequence sets (0, abc, 5:, 1:2:3)RFC 3501 section 9
Message sequence numbers greater than the number of messages in FETCH, STORE, COPY and MOVE, also * in an empty mailbox. UID sets and SEARCH keys can point past the endRFC 3501 section 9, seq-number
Flags that are not atoms, \Recent in STORE or APPEND, invalid datesRFC 3501 section 9
ENABLE after SELECT or EXAMINERFC 5161 section 3.1
ID lists that break the limits of RFC 2971RFC 2971
More than one message in APPEND without MULTIAPPEND. With MULTIAPPEND, a zero-length message literal cancels the whole APPEND with NORFC 3502
Command lines longer than 1 MiB. RFC 2683 asks servers to accept at least 8000 octets, and clients to stay near 1000RFC 2683 section 3.2.1.5

Framing, literals and pipelining​

RuleReference
Command lines that end with a bare LF instead of CRLFRFC 3501 section 9
Literal data sent before the server's + continuation requestRFC 3501 section 4.3
{n+} without LITERAL+ or LITERAL-. With LITERAL-, a non-synchronizing literal over 4096 octets gets BAD [TOOBIG]RFC 7888 section 5
Literals for unknown commands, or for commands that can not run in the current state, are refused without a continuation requestRFC 3501 section 4.3
Literal8 (~{n}) anywhere but in an APPEND or REPLACE message with BINARY, or a SETMETADATA value with METADATA, refused without a continuation request. NUL octets in a normal literal. A literal8 TEXT part in CATENATERFC 3516, RFC 4469 section 5
Pipelined commands that RFC 3501 calls ambiguous, for example CHECK followed by FETCH without waiting for the CHECK resultRFC 3501 section 5.5
STARTTLS and COMPRESS with commands pipelined after them. TLS or compression is not started and the pipelined commands are refused without running. COMPRESS while compression is active gets BAD [COMPRESSIONACTIVE]RFC 9051 section 6.2.1, RFC 4978 section 3
Anything other than DONE while IDLERFC 2177

Two of these, each pair sent in a single write (<LF> marks a bare line feed):

C: A3 CHECK
C: A4 FETCH 1 FLAGS
S: A3 OK Completed
S: A4 BAD Commands with message sequence numbers must wait for the completion of earlier commands
C: A5 NOOP<LF>
C: A6 NOOP
S: A5 BAD Lines must end with CRLF
S: A6 OK Completed

Mailbox names​

RuleReference
Names that are not valid modified UTF-7, including 8-bit namesRFC 3501 section 5.1.3
CREATE or RENAME to names with an empty hierarchy level (foo//bar, /foo, foo//), answered with NO [CANNOT]RFC 5530 section 3

Search, sort and thread​

RuleReference
8-bit SEARCH strings without CHARSET UTF-8, invalid UTF-8. An unsupported charset gets NO [BADCHARSET]RFC 3501 section 6.4.4
SORT and THREAD with a charset that is not an atom or a quoted string, an empty sort criteria list, REVERSE that is not followed by a sort key (REVERSE REVERSE DATE), or a threading algorithm that is not an atomRFC 5256 section 5
Unknown SEARCH RETURN options or RETURN after CHARSET, $ combined with numbers, and SEARCH MODSEQ values or entry names that break the grammarRFC 4466 section 2.6.1, RFC 5182, RFC 7162
UIDAFTER and UIDBEFORE (MESSAGELIMIT) with anything but a single UIDRFC 9738 section 3.2

Authentication​

RuleReference
Invalid base64 in SASL exchangesRFC 3501 section 9
8-bit user names or passwords in LOGIN (UTF-8 user names need AUTHENTICATE), invalid UTF-8 in an AUTHENTICATE PLAIN messageRFC 9755 section 5, RFC 4616 section 2
OAUTHBEARER client responses that break the RFC 7628 or GS2 grammar, and anything other than a single %x01 after an OAUTHBEARER error resultRFC 7628, RFC 5801

See Authentication for transcripts.

Extensions​

These apply when the plugin is loaded. Without it, the extension's commands and arguments are unknown and get BAD anyway.

ExtensionRule
QRESYNC (RFC 7162)The QRESYNC SELECT parameter or the VANISHED modifier without ENABLE QRESYNC, VANISHED with FETCH or without CHANGEDSINCE, and values that break the grammar: UIDVALIDITY or mod-sequence 0, * in the UID sets, sequence match sets that are not ascending or not of the same size
LIST-EXTENDED (RFC 5258)Unknown options, RECURSIVEMATCH without a base option like SUBSCRIBED (also (SPECIAL-USE RECURSIVEMATCH), RFC 6154 section 6), an empty pattern list, options with values they do not take, a repeated STATUS return option with different items, invalid STATUS items (RFC 5819)
CREATE-SPECIAL-USE (RFC 6154 section 6)USE entries that are not an atom starting with a backslash (use-attr-ext = "\" atom), like NIL, quoted strings, literals or Sent. An attribute the server does not support gets NO [USEATTR] (section 3)
METADATA (RFC 5464 section 3.2)Entry names with //, a trailing /, *, %, 8-bit or control characters, or a scope other than /private or /shared, values that are atoms or use bare CR or LF as line ends, empty entry or option lists, and GETMETADATA options after the mailbox name (errata 2785)
ACL (RFC 4314 section 3)Unknown or uppercase rights, empty identifiers, identifiers with control characters or invalid UTF-8
CATENATE (RFC 4469)URLs that are not absolute-path references (/INBOX/;UID=1), including relative-path references like ;UID=1 that RFC 5092 section 7.2 forbids, and URLs of message parts that do not exist (NO [BADURL ...])
UIDONLY (RFC 9586)Every command that takes message sequence numbers, and sequence sets in search criteria, get BAD [UIDREQUIRED]
UTF8=ACCEPT (RFC 9755)Invalid UTF-8 in quoted strings, SEARCH CHARSET after ENABLE UTF8=ACCEPT, mailbox names with control characters (in UTF-8, or encoded in modified UTF-7 like &AA0-), U+2028, U+2029, a leading BOM, unassigned code points or a name that is not in Unicode Normalization Form C. NO for APPEND of a message with an 8-bit header before ENABLE UTF8=ACCEPT (section 4)
BINARY (RFC 3516)BINARY[], BINARY of multipart or message/rfc822 parts (RFC 9051 section 6.4.5 allows leaf body parts only), HEADER, TEXT or MIME sections, and a partial range on BINARY.SIZE
NOTIFY (RFC 5465)MessageNew without MessageExpunge or the other way round, FlagChange without both (section 5), mailbox events or two selected filters with selected/selected-delayed (section 6.1), fetch attributes outside the selected filters, empty event or mailbox lists, NOTIFY SET without event groups. Unknown events get NO [BADEVENT (...)] listing the supported ones (section 3.1)
IMAP4rev2 (RFC 9051)After ENABLE IMAP4rev2: CHECK, LSUB, the RFC822, RFC822.HEADER and RFC822.TEXT FETCH items, the NEW, OLD and RECENT SEARCH keys and the RECENT STATUS item (none are in the RFC 9051 grammar, Appendix E), numbers above 63 bits in LARGER and SMALLER, invalid UTF-8, and mailbox names that are not Net-Unicode (section 5.1). Before it: 8-bit quoted strings (Appendix A), and partial ranges, LARGER and SMALLER values above 32 bits

Response codes​

Failures carry the RFC 5530 response codes that RFC 9051 section 7.1 lists, in both protocol revisions:

CodeWhen
AUTHENTICATIONFAILED, AUTHORIZATIONFAILEDFailed logins
ALREADYEXISTS, NONEXISTENT, CANNOT, HASCHILDREN, NOPERMMailbox operations
TRYCREATEThe target of APPEND, COPY or MOVE does not exist or is a \Noselect name
CLIENTBUGSTATUS on the selected mailbox, and STORE, EXPUNGE, UID EXPUNGE, MOVE and REPLACE in a mailbox selected read-only
EXPUNGEISSUEDFETCH, STORE, SEARCH, SORT or THREAD completes while the EXPUNGE of another session can not be reported yet, see Multiple sessions

RFC 2683 recommendations​

A few client side recommendations of RFC 2683 are checked as well:

  • STATUS on the selected mailbox gets CLIENTBUG (section 3.1.1).
  • Mailbox names must be valid modified UTF-7 (section 3.4.2).
  • EXPUNGE or STORE after EXAMINE, which answered [READ-ONLY], get NO (section 3.3.2).

The other direction​

Strictness works both ways. ImapKit's own responses follow the grammar too: a string that can not be quoted is sent as a literal, and every status response has human readable text.

ImapKit's test suite enforces this. Every transcript a test produces goes through a response grammar check before the test looks at it: CRLF framing and literals, the RFC 3501 section 9 shape of tagged, untagged and continuation responses, and ImapFlow's response parser. A fuzz test replays mutated commands under the same check. So if your client fails to parse something ImapKit sent, look at the client's parser first.

The flip side: ImapKit does not imitate the quirks of real servers by default. To test how your client copes with a server that misbehaves, use Scripted faults and Quirk presets.