Case-insensitive IMAP response parsing (RFC 3501) - #116
Conversation
Adds UIDPLUS extension support so callers can recover the UIDs the server assigns on APPEND/COPY and target expunges by UID: - appendFullUID: like appendFull, returns the APPENDUID response code - copyUID / copyUIDs / copyUIDR: UID COPY returning the COPYUID code - uidExpunge / uidExpungeR: UID EXPUNGE over a UID set or range New types AppendUID and CopyUID, the UIDSet alias, and the APPENDUID/COPYUID/UIDNOTSTICKY status codes with their parsers. sendCommandWithResponse exposes the tagged ServerResponse so the response codes can be read. The existing appendFull/copyFull keep their old signatures by discarding the UID result. Covered by new parser cases in baseTest and a dedicated imapUIDPlusTest group exercising the API against scripted server responses.
RFC 3501 keywords, response codes, flag names and status attributes are case-insensitive, but the parser matched them with case-sensitive `string`, so any server replying with non-canonical casing (e.g. `ok`, `* search`, `[uidvalidity ...]`, `\seen`) caused a parse error. Adds `stringCI`/`charCI` and applies them throughout the response parser. Along the way the tagged/fatal response handling is factored into `pWithTaggedOrFatal`, `pDone` is split into `pRespCode`/`pRespText`/`pStatusCode`, and string parsing is unified (`pQuotedString`/`pLiteralString`/`pAString`/`pMailboxName`). This also: - surfaces an untagged `* BYE` as a fatal response instead of failing - accepts an empty `* SEARCH` reply (no matches) - accepts a `NIL` hierarchy separator in LIST/LSUB - stops `atomChar` from running past CR/LF Covered by a new caseInsensitiveTest group.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 11750ce623
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pWithTaggedOrFatal :: Parser RespDerivs (Either (String, Integer) b) | ||
| -> ([b] -> v) | ||
| -> Parser RespDerivs (ServerResponse, MboxUpdate, v) | ||
| pWithTaggedOrFatal lineParser build = tagged <|> fatal |
There was a problem hiding this comment.
Detect BYE after preceding untagged lines
When a response buffer contains any untagged response before * BYE (for example, * 1 EXISTS followed by * BYE), the tagged branch consumes both lines through the generic pOtherLine fallback and then fails in pDone; the top-level fatal alternative restarts at the first line, where pFatalLine also fails. Consequently eval raises a parse error rather than returning the intended fatal BAD response. BYE detection needs to occur while processing each untagged line, not only as an alternative at the initial cursor.
Useful? React with 👍 / 👎.
| char ')' | ||
| return attrs | ||
| parseSep = space >> char '"' >> anyChar `manyTill` char '"' | ||
| parseSep = space >> ((string "NIL" >> return "") <|> pQuotedString) |
There was a problem hiding this comment.
Match NIL hierarchy separators case-insensitively
For a legal lowercase response such as * LIST () nil INBOX, this case-sensitive match makes pListLine fail; pOtherLine then consumes the response, so list silently omits the mailbox. Since the change promises case-insensitive IMAP protocol keywords, the NIL alternative should use stringCI as the surrounding LIST tokens do.
Useful? React with 👍 / 👎.
| sequence $ replicate num anyChar | ||
|
|
||
| pAString :: Parser RespDerivs String | ||
| pAString = pQuotedString <|> pLiteralString <|> many1 atomChar |
There was a problem hiding this comment.
Allow closing brackets in unquoted astrings
When a LIST/LSUB response contains a legal unquoted mailbox such as foo]bar, the new shared astring parser rejects it because atomChar excludes ]. IMAP's astring grammar explicitly adds the ] response-special back to its permitted characters, and the previous mailbox parser accepted every non-space character, so this regression causes the LIST row to be discarded by pOtherLine. Use a dedicated astring-character parser that permits ] rather than reusing atomChar.
Useful? React with 👍 / 👎.
| pAtomValue = do v <- many1 atomChar | ||
| return $ if map toUpper v == "NIL" then "" else v |
There was a problem hiding this comment.
Preserve scalar NIL fetch values
For a FETCH item whose value is scalar NIL, such as BODY[] NIL, the exported pFetch parser now returns an empty string instead of the previous literal "NIL". This loses the distinction between an absent value and a zero-length literal and is inconsistent with the streaming FETCH parser used by fetchByString, which preserves NIL. Keep the parsed atom unchanged and let higher-level callers interpret NIL when appropriate.
Useful? React with 👍 / 👎.
Stacked on #115 (UIDPLUS). This branch is based on the UIDPLUS branch, so until #115 merges the diff here also shows those commits — the change that belongs to this PR is the single commit "Make IMAP response parsing case-insensitive (RFC 3501)". Please review/merge #115 first; I'll rebase this onto
masterafterwards so the diff narrows to just the parser change.What
RFC 3501 keywords, response codes, flag names and status attributes are case-insensitive, but the response parser matched them with case-sensitive
string. A server replying with non-canonical casing —a001 ok,* search …,[uidvalidity …],\seen— caused a hard parseerror. Several real-world servers (Exchange/Office365 among them) don't use the exact canonical uppercase.This adds
stringCI/charCIand applies them throughout the parser.While restructuring, also fixed
pWithTaggedOrFatalpDoneintopRespCode/pRespText/pStatusCodepQuotedString/pLiteralString/pAString/pMailboxName* BYEas a fatal response (BAD) instead of failing to parse* SEARCHreply (zero matches)NILhierarchy separator inLIST/LSUBatomCharfrom running past CR/LFTests
New
caseInsensitiveTestgroup: lowercase tagged/bracketed status codes, lowercase flags, untagged BYE, empty SEARCH, lowercase SEARCH keyword, NIL LIST separator.cabal testpasses (GHC 9.8.4).🤖 Generated with Claude Code