From 739c0b5d3b6fe34643dcdb439b483f07dc766d77 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:08:56 +0200 Subject: [PATCH] Fix JMAP: Methods are only available if their capability is in `using` --- CHANGELOG.md | 1 + crates/jmap-proto/src/request/capability.rs | 6 +++ crates/jmap-proto/src/request/method.rs | 26 +++++++++++++ crates/jmap-proto/src/request/parser.rs | 42 ++++++++++++++++++++- crates/jmap/src/api/request.rs | 19 +++++++++- 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec365ab..b41fdf92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - JMAP conformance (pass the [jmap-test-suite](https://github.com/jmapio/jmap-test-suite) tests): + - Methods are only available if their capability is in `using`. - Default calendars and address books are not subscribed by default. - `*/set`: Unchanged immutable `id` property is rejected on update. - `*/query` and `*/queryChanges`: null` rejected as `notRequest`. diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 527b7d0f..f8e180dd 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -97,6 +97,12 @@ pub enum Capability { #[repr(transparent)] pub struct CapabilityIds(pub u32); +impl CapabilityIds { + pub fn contains(&self, capability: Capability) -> bool { + self.0 & capability as u32 != 0 + } +} + #[derive(Debug, Clone, serde::Serialize)] #[serde(untagged)] #[allow(dead_code)] diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index 0334585d..f7880819 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::request::capability::Capability; use registry::{ schema::prelude::{OBJ_SINGLETON, ObjectType}, types::EnumImpl, @@ -42,6 +43,31 @@ pub enum MethodObject { Registry(ObjectType), } +impl MethodObject { + pub fn capability(&self) -> Capability { + match self { + MethodObject::Email + | MethodObject::Mailbox + | MethodObject::Thread + | MethodObject::SearchSnippet => Capability::Mail, + MethodObject::Core | MethodObject::PushSubscription => Capability::Core, + MethodObject::Blob => Capability::Blob, + MethodObject::Identity | MethodObject::EmailSubmission => Capability::Submission, + MethodObject::VacationResponse => Capability::VacationResponse, + MethodObject::SieveScript => Capability::Sieve, + MethodObject::Principal | MethodObject::ShareNotification => Capability::Principals, + MethodObject::Quota => Capability::Quota, + MethodObject::Calendar + | MethodObject::CalendarEvent + | MethodObject::CalendarEventNotification + | MethodObject::ParticipantIdentity => Capability::Calendars, + MethodObject::AddressBook | MethodObject::ContactCard => Capability::Contacts, + MethodObject::FileNode => Capability::FileNode, + MethodObject::Registry(_) => Capability::Stalwart, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MethodFunction { Get, diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index b7603621..7f09e727 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -11,7 +11,7 @@ use super::{ use crate::request::{ CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, QueryRequestMethod, SetRequestMethod, - deserialize::{DeserializeArguments, deserialize_request}, + deserialize::DeserializeArguments, }; use serde::{ Deserialize, Deserializer, @@ -713,7 +713,45 @@ impl<'de> Deserialize<'de> for Request<'de> { where D: Deserializer<'de>, { - deserialize_request(deserializer) + struct RequestVisitor; + + impl<'de> Visitor<'de> for RequestVisitor { + type Value = Request<'de>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a JMAP request object") + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut target = Request::default(); + let mut has_using = false; + let mut has_method_calls = false; + + while let Some(key) = map.next_key::<&str>()? { + match key { + "using" => has_using = true, + "methodCalls" => has_method_calls = true, + _ => {} + } + target + .deserialize_argument(key, &mut map) + .map_err(de::Error::custom)?; + } + + if !has_using || !has_method_calls { + return Err(de::Error::custom( + "Request is missing the \"using\" or \"methodCalls\" property.", + )); + } + + Ok(target) + } + } + + deserializer.deserialize_map(RequestVisitor) } } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 748a6f10..b815df82 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -50,7 +50,7 @@ use http_proto::HttpSessionData; use jmap_proto::{ request::{ Call, CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryRequestMethod, Request, - RequestMethod, SetRequestMethod, method::MethodName, + RequestMethod, SetRequestMethod, capability::Capability, method::MethodName, }, response::{Response, ResponseMethod, SetResponseMethod}, }; @@ -85,6 +85,7 @@ impl RequestHandler for Server { session: &HttpSessionData, ) -> Response<'x> { let add_created_ids = request.created_ids.is_some(); + let using = request.using; let mut response = Response::new( access_token.state(), request.created_ids.unwrap_or_default(), @@ -102,6 +103,22 @@ impl RequestHandler for Server { continue; } + if !matches!(call.method, RequestMethod::Error(_)) { + let capability = call.name.obj.capability(); + if capability != Capability::Stalwart && !using.contains(capability) { + response.push_response( + call.id, + MethodName::error(), + trc::JmapEvent::UnknownMethod.into_err().details(format!( + "Method {} requires capability {} which is not present in the \"using\" property.", + call.name, + capability.as_str() + )), + ); + continue; + } + } + loop { let mut next_call = None;