Skip to content

Validate response text, command tags and namespace delimiters - #866

Open
danieleggert wants to merge 3 commits into
harden-3-tagged-ext-labelfrom
harden-4-text-and-tags
Open

danieleggert wants to merge 3 commits into
harden-3-tagged-ext-labelfrom
harden-4-text-and-tags

Conversation

@danieleggert

@danieleggert danieleggert commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

[4/6] One of the design goals of this library is that the API types can only represent valid state. But that doesn’t hold true today. These 6 stacked PRs are smaller steps towards that goal. #863 #864 #865 #866 #867 #868

Motivation:

Response text, tags and namespace delimiters were written raw: .no(.init(text: "No mailbox \(name)")) emits any CRLF in name.

Modifications:

  • ResponseText.Text allows UTF-8 (RFC 9051) but no leading space, which the parser drops.
  • A leading [ without a code stays allowed: IMAP can't escape it, and the parser produces it.
  • Command.Tag is shared by commands and responses.
  • NamespaceDescription.init throws, like MailboxPath.

`.no(.init(text: "No mailbox x\r\n* BYE pwned"))` was written verbatim,
injecting an untagged response; likewise tagged, BYE, PREAUTH and
continuation responses.

RFC 9051: `text = 1*(TEXT-CHAR / UTF8-2 / UTF8-3 / UTF8-4)`. New
`ResponseText.Text` replaces the `String`; `init?(_:)` rejects NUL, CR,
LF and a leading space (the parser skips it), literals trap. Empty stays
valid. The parser now accepts UTF-8 text.
`TaggedCommand(tag: "A1 NOOP\r\nA2 DELETE INBOX\r\nA3", command: .noop)`
was written verbatim, injecting a command; likewise `APPEND` start.
`TaggedResponse(tag: "* BYE x\r\nA1", …)` injected a response.

RFC 3501: `tag = 1*<any ASTRING-CHAR except "+">`. New `Command.Tag`
replaces the `String` in `TaggedCommand`, `TaggedResponse`, `Response`,
`CommandStreamPart`, `AppendCommand`, `BadCommand` and NIOIMAP's state
machine errors; `init?(_:)` validates, literals trap. The parser already
enforces the production. IMAPCommands converts its numeric tags.
`NamespaceDescription(string: "x", char: "\r\n", …)` wrote a raw CRLF
inside `"…"`, injecting a response. `"\""` encoded as `"""`, and `"\\"`
as `"\"` (unterminated); non-ASCII was accepted.

RFC 2342: `DQUOTE QUOTED-CHAR DQUOTE / nil`. `init(string:delimiter:
responseExtensions:)` (was `char:`) throws `InvalidDelimiterError` for
non-ASCII, CR, LF, NUL; the delimiter is stored as a byte, read-only.
Quoted-specials are escaped on write and parsed when escaped.
@danieleggert
danieleggert force-pushed the harden-4-text-and-tags branch from 8f7d28a to 76a8328 Compare October 24, 2026 09:37
@danieleggert
danieleggert marked this pull request as ready for review October 24, 2026 09:39

@mdiep mdiep left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Command.Tag is great. ❤️

/// is a bug to be caught on first run rather than handled.
///
/// - SeeAlso: [RFC 9051 Section 9](https://datatracker.ietf.org/doc/html/rfc9051#section-9)
public struct Text: Hashable, Sendable {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this one adds value. I'd just leave it as a String.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants