Skip to content

Validate IMAP URL components - #867

Open
danieleggert wants to merge 7 commits into
harden-4-text-and-tagsfrom
harden-5-url-family
Open

danieleggert wants to merge 7 commits into
harden-4-text-and-tagsfrom
harden-5-url-family

Conversation

@danieleggert

@danieleggert danieleggert commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

[5/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:

[REFERRAL] writes URL parts raw: IMAPServer(host: "h] x\r\n* BYE").

Modifications:

Start with URLCharacterClass (one validator for RFC 5092/3986) and IMAPServer.Host. The Encoded* conversions are mechanical.

Also fixes round-trips: [::1] lost its brackets, ;AUTH=* became .any, port was Int, and the URLFETCH response was parenthesized.

Comment on lines +70 to +73
func normalizedURLComponent(
_ characterClass: URLCharacterClass,
emptiness: URLComponentEmptiness = .rejected
) -> String? {

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.

Since this can return nil, it seems like it's doing more than normalization.

@danieleggert
danieleggert force-pushed the harden-5-url-family branch 2 times, most recently from 3c588a1 to bf278ad Compare October 30, 2026 14:48
`EncodedMailbox(mailbox: "x] x\r\n* BYE")` was written verbatim inside
the bare URL of `[REFERRAL …]`, injecting a response; likewise
`EncodedUser`, `EncodedAuthenticationType`, `EncodedSearch`,
`EncodedSection`.

RFC 5092: `enc-user`/`enc-auth-type = 1*achar`, `enc-mailbox`/
`enc-search`/`enc-section = 1*bchar`. Each type's public `var` and
labelled init become `init?(_:)`, literals trap, `String.init` reads.
One helper validates by character class and `pct-encoded` triplet, and
uppercases hex digits as the parser does, so values round-trip.
`EncodedAuthenticatedURL(data: "x\r\n* BYE")` was written verbatim at the
end of a URLAUTH URL, injecting a response.

RFC 4467: `enc-urlauth = 32*HEXDIG`. `init(data:)` and the public `var`
become `init?(_:)`; literals trap; `String.init` reads. The parser read
exactly 32 digits, so longer tokens didn't round-trip; it now reads all.
`IMAPServer(host: "h] x\r\n* BYE")` was written verbatim in the bare URL
of `[REFERRAL …]`, injecting a response. `imap://[::1]/` parsed as host
`::1` and re-encoded as `imap://::1/`. A negative port encoded as `:-1`.

RFC 3986: `host = IP-literal / IPv4address / reg-name`. New nested
`IMAPServer.Host` replaces the `String`: `init?(_:)` validates IPv6,
IPvFuture and reg-name (IPv4 is a subset), literals trap. The parser
keeps the brackets, accepts all of reg-name, and validates via `init?`;
its unreachable IPv4 branch is gone. `port` is now `UInt16?`.
`.urlFetch([.init(url: "url", data: nil)])` encoded as
`URLFETCH("url" NIL)`, which our parser rejects.

RFC 4467: `urlfetch-data = "URLFETCH" 1*(SP url-full SP nstring)`. Write
each pair after a space, unparenthesized. Fix fixtures that enshrined
the old output.
`writeAuthIMAPURLRump`, `writeAuthIMAPURLFull`, `writeRelativeIMAPURL`,
`writeURLFetchType`, `writeInternetMessageDate` and
`writeListSelectBaseOptionQuoted` had no callers outside their own
encode tests, nor did the writers only they reached
(`writeAuthenticatedURL(NetworkMessagePath)`, `writeNetworkPath`,
`writeAbsoluteMessagePath`, `writeIUIDOnly`,
`writeURLMessageSectionOnly`, `writeMessagePathByteRangeOnly`). Delete
them and those tests; parse tests stay.
`imap://h/INBOX/;UID=1/;SECTION=1/` parsed with section `1`, so
`EncodedSection("1/")` didn't round-trip: the parser dropped the `/`
even when no `;PARTIAL=` followed it. Drop it only when one does.
`.type("*")` encoded as `;AUTH=*`, which parses as `.any`; `.type("*x")`
failed to parse, since `*` matched first.

RFC 5092: `iauth = ";AUTH=" ( "*" / enc-auth-type )`. `init?(_:)`
rejects `*`; the parser tries the longer `enc-auth-type` first.
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