From f95c64c6985e452941a3782908ac479100814471 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 26 Oct 2026 08:40:59 -0700 Subject: [PATCH] JMAP: an unknown capability in "using" is unknownCapability A request naming a capability the server doesn't offer failed as a whole parse error, so the client got notRequest with serde's message. RFC 8620 section 3.6.1 says unknownCapability, and a client that checks for it can tell the user what's wrong (an old capability name, a feature this server doesn't have). "using" now parses without failing and keeps the first unknown name; Request::parse and WebSocketMessage::parse then fail with JmapEvent::UnknownCapability, which already maps to the error type, with the name as its detail. --- crates/jmap-proto/src/request/capability.rs | 39 ++++++++------ crates/jmap-proto/src/request/mod.rs | 3 ++ crates/jmap-proto/src/request/parser.rs | 51 +++++++++++++++++- crates/jmap-proto/src/request/websocket.rs | 12 +++++ .../test_privacy_check.cpython-314.pyc | Bin 0 -> 13615 bytes 5 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 tools/fork/tests/__pycache__/test_privacy_check.cpython-314.pyc diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index becfccf8..aaa2d0ba 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -553,15 +553,24 @@ impl Capability { } } -impl<'de> Deserialize<'de> for CapabilityIds { +// inbuxa: a request's `using`. A capability the server doesn't know is kept, +// not a parse error, so the request fails with unknownCapability naming it +// (RFC 8620, section 3.6.1) instead of notRequest. +#[derive(Debug, Clone, Default)] +pub struct Using { + pub ids: CapabilityIds, + pub unknown: Option, +} + +impl<'de> Deserialize<'de> for Using { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - struct CapabilityIdsVisitor; + struct UsingVisitor; - impl<'de> serde::de::Visitor<'de> for CapabilityIdsVisitor { - type Value = CapabilityIds; + impl<'de> serde::de::Visitor<'de> for UsingVisitor { + type Value = Using; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("an array of capability strings") @@ -571,23 +580,23 @@ impl<'de> Deserialize<'de> for CapabilityIds { where A: serde::de::SeqAccess<'de>, { - let mut capability_flags = 0u32; + let mut using = Using::default(); while let Some(capability_str) = seq.next_element::>()? { - let capability = - Capability::parse(capability_str.as_ref()).ok_or_else(|| { - serde::de::Error::custom(format!( - "Unknown capability: {capability_str:?}" - )) - })?; - - capability_flags |= capability as u32; + match Capability::parse(capability_str.as_ref()) { + Some(capability) => using.ids.0 |= capability as u32, + None => { + if using.unknown.is_none() { + using.unknown = Some(capability_str.into_owned()); + } + } + } } - Ok(CapabilityIds(capability_flags)) + Ok(using) } } - deserializer.deserialize_seq(CapabilityIdsVisitor) + deserializer.deserialize_seq(UsingVisitor) } } diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 9a366102..9b54a3c6 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -61,6 +61,8 @@ where #[derive(Debug)] pub struct Request<'x> { pub using: CapabilityIds, + // inbuxa: the first capability in `using` the server doesn't know. + pub unknown_capability: Option, pub method_calls: Vec>>, pub created_ids: Option>, } @@ -298,6 +300,7 @@ impl Default for Request<'_> { fn default() -> Self { Request { using: CapabilityIds::default(), + unknown_capability: None, method_calls: Vec::new(), created_ids: None, } diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index b93f3e2f..c970b083 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -12,7 +12,7 @@ use super::{ }; use crate::request::{ CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, - QueryRequestMethod, SetRequestMethod, deserialize::DeserializeArguments, + QueryRequestMethod, SetRequestMethod, capability::Using, deserialize::DeserializeArguments, }; use serde::{ Deserialize, Deserializer, @@ -24,6 +24,12 @@ impl<'x> Request<'x> { pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result { if json.len() <= max_size { match serde_json::from_slice::(json) { + Ok(Request { + unknown_capability: Some(capability), + .. + }) => Err(trc::JmapEvent::UnknownCapability + .into_err() + .details(capability)), Ok(request) => { if request.method_calls.len() <= max_calls { Ok(request) @@ -49,7 +55,9 @@ impl<'de> DeserializeArguments<'de> for Request<'de> { { hashify::fnc_map!(key.as_bytes(), b"using" => { - self.using = map.next_value()?; + let using = map.next_value::()?; + self.using = using.ids; + self.unknown_capability = using.unknown; }, b"methodCalls" => { self.method_calls = map.next_value()?; @@ -1339,6 +1347,45 @@ mod tests { println!("{:#?}", Request::parse(TEST2.as_bytes(), 10, 10240)); } + // inbuxa: an unknown capability fails the request as unknownCapability, + // naming it, and a known one parses as before. + #[test] + fn unknown_capability_is_named() { + let request = r#"{"using": ["urn:ietf:params:jmap:core", "urn:example:nope", "urn:example:other"], + "methodCalls": [["Core/echo", {}, "0"]]}"#; + let err = Request::parse(request.as_bytes(), 10, 10240).unwrap_err(); + assert_eq!( + *err.as_ref(), + trc::EventType::Jmap(trc::JmapEvent::UnknownCapability) + ); + assert_eq!( + err.value(trc::Key::Details).and_then(|v| v.as_str()), + Some("urn:example:nope") + ); + + let request = r#"{"using": ["urn:ietf:params:jmap:core", "urn:inbuxa:jmap:registry"], + "methodCalls": [["Core/echo", {}, "0"]]}"#; + let request = Request::parse(request.as_bytes(), 10, 10240).unwrap(); + assert!( + request + .using + .contains(crate::request::capability::Capability::Stalwart) + ); + assert!(request.unknown_capability.is_none()); + } + + #[test] + fn unknown_capability_over_websocket_is_named() { + let message = r#"{"@type": "Request", "id": "r1", "using": ["urn:example:nope"], + "methodCalls": [["Core/echo", {}, "0"]]}"#; + let err = crate::request::websocket::WebSocketMessage::parse(message.as_bytes(), 10, 10240) + .unwrap_err(); + assert_eq!( + *err.as_ref(), + trc::EventType::Jmap(trc::JmapEvent::UnknownCapability) + ); + } + #[test] fn parse_method_name_with_escaped_solidus() { let request = Request::parse(TEST_ESCAPED_SOLIDUS.as_bytes(), 10, 10240) diff --git a/crates/jmap-proto/src/request/websocket.rs b/crates/jmap-proto/src/request/websocket.rs index 8726bcd8..a6bea2b2 100644 --- a/crates/jmap-proto/src/request/websocket.rs +++ b/crates/jmap-proto/src/request/websocket.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::Request; @@ -107,6 +109,16 @@ impl<'x> WebSocketMessage<'x> { pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result { if json.len() <= max_size { match serde_json::from_slice::(json) { + Ok(WebSocketMessage::Request(WebSocketRequest { + request: + Request { + unknown_capability: Some(capability), + .. + }, + .. + })) => Err(trc::JmapEvent::UnknownCapability + .into_err() + .details(capability)), Ok(WebSocketMessage::Request(req)) if req.request.method_calls.len() > max_calls => { diff --git a/tools/fork/tests/__pycache__/test_privacy_check.cpython-314.pyc b/tools/fork/tests/__pycache__/test_privacy_check.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d748bdd6b9739f5111279be2e83aa8ac7a29251 GIT binary patch literal 13615 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 L(Md6@30D6HM5&g4 literal 0 HcmV?d00001 -- 2.54.0