From 9429f1de0054ef3cd733adf4e33d0bab1581c458 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 5 Oct 2026 14:15:39 -0700 Subject: [PATCH] Refuse an empty JMAP id instead of reading it as id 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Email/set with mailboxIds {"": true} was accepted and filed the message in the Inbox. Id::from_str returned 0 for an empty string, and document 0 is each collection's first: the Inbox for mail. RFC 8620 §1.2 ids are 1 to 255 characters, so "" is refused now, and every caller already treats a refused id as invalid or not found. Over-long ids still parse as they did; upstream's test accepts them on purpose. Found while probing group mailboxes on a scratch server (specs/multi-account.md, G3). types tests, jmap_tests and imap_tests pass (RocksDB). --- crates/types/src/id.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/types/src/id.rs b/crates/types/src/id.rs index 2befedc..d598f34 100644 --- a/crates/types/src/id.rs +++ b/crates/types/src/id.rs @@ -36,6 +36,13 @@ impl FromStr for Id { type Err = (); fn from_str(s: &str) -> Result { + // inbuxa: an empty id is not id 0. RFC 8620 §1.2 ids are 1 to 255 + // characters, and "" would otherwise name each collection's first + // document: `mailboxIds: {"": true}` filed a message in the Inbox. + if s.is_empty() { + return Err(()); + } + let mut id = 0; for &ch in s.as_bytes() { @@ -261,4 +268,10 @@ mod tests { Id::from_str("p333333333333p333333333333").unwrap(); } + + #[test] + fn empty_jmap_id_is_refused() { + assert!(Id::from_str("").is_err()); + assert_eq!(Id::from_str("a").unwrap(), Id::from(0u64)); + } }