Fix JMAP: Methods are only available if their capability is in using

This commit is contained in:
Maurus Decimus
2026-06-18 18:08:56 +02:00
parent 7c4d1df9ba
commit 739c0b5d3b
5 changed files with 91 additions and 3 deletions

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
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)
}
}

View File

@@ -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;