Take the registry capability from the server's session

Registry calls (x:Account, x:Domain and the rest of the x: types) were
always sent under urn:stalwart:jmap. inbuxa advertises the same registry
as urn:inbuxa:jmap:registry, so the capability now comes from the connected
server: Session::registry_urn() picks urn:inbuxa:jmap:registry when the
session advertises it, else urn:stalwart:jmap, so Stalwart servers still
work as a source. A request carrying an x: call against a server that
advertises neither fails with a MissingCapability error naming both,
treated like any other connection-level failure, instead of sending a
capability the server never offered.
This commit is contained in:
2026-09-30 10:09:10 -07:00
parent 5931b06392
commit 7fbcf5031e
3 changed files with 135 additions and 19 deletions
+86 -18
View File
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
@@ -17,9 +18,28 @@ use crate::logging::Logger;
pub const URN_CORE: &str = "urn:ietf:params:jmap:core";
pub fn using_urn(method: &str) -> &'static str {
/// The capability inbuxa advertises for its registry types (`x:Account`,
/// `x:Domain` and the rest of the `x:` objects).
pub const URN_INBUXA_REGISTRY: &str = "urn:inbuxa:jmap:registry";
/// The same registry as Stalwart advertises it, so a Stalwart server can be
/// migrated from.
pub const URN_STALWART_REGISTRY: &str = "urn:stalwart:jmap";
/// The capability a method needs. Registry (`x:`) methods have no fixed
/// capability: the connected server names it, and `registry` carries that
/// name (see `Session::registry_urn`). Without one they cannot be sent.
pub fn using_urn(method: &str, registry: Option<&'static str>) -> Result<&'static str, JmapError> {
let prefix = method.split('/').next().unwrap_or(method);
match prefix {
if prefix.starts_with("x:") {
return registry.ok_or_else(|| {
JmapError::MissingCapability(format!(
"{method} needs the server's registry capability, but it advertises neither \
{URN_INBUXA_REGISTRY} nor {URN_STALWART_REGISTRY}"
))
});
}
Ok(match prefix {
"Mailbox" | "Email" => "urn:ietf:params:jmap:mail",
"Identity" => "urn:ietf:params:jmap:submission",
"SieveScript" => "urn:ietf:params:jmap:sieve",
@@ -27,9 +47,8 @@ pub fn using_urn(method: &str) -> &'static str {
"Calendar" | "CalendarEvent" | "ParticipantIdentity" => "urn:ietf:params:jmap:calendars",
"FileNode" => "urn:ietf:params:jmap:filenode",
"Principal" => "urn:ietf:params:jmap:principals",
"x:Account" | "x:Domain" => "urn:stalwart:jmap",
_ => URN_CORE,
}
})
}
#[derive(Debug, Clone)]
@@ -42,11 +61,19 @@ pub struct MethodCall {
#[derive(Debug, Clone, Default)]
pub struct Request {
calls: Vec<MethodCall>,
registry: Option<&'static str>,
}
impl Request {
pub fn new() -> Request {
Request { calls: Vec::new() }
Request::default()
}
/// The registry capability to use for any `x:` call in this request,
/// normally `session.registry_urn()`.
pub fn registry(&mut self, urn: Option<&'static str>) -> &mut Request {
self.registry = urn;
self
}
pub fn call(
@@ -71,29 +98,29 @@ impl Request {
self.calls.is_empty()
}
pub fn using(&self) -> Vec<String> {
pub fn using(&self) -> Result<Vec<String>, JmapError> {
let mut set: IndexSet<String> = IndexSet::new();
set.insert(URN_CORE.to_owned());
for c in &self.calls {
set.insert(using_urn(&c.name).to_owned());
set.insert(using_urn(&c.name, self.registry)?.to_owned());
}
set.into_iter().collect()
Ok(set.into_iter().collect())
}
fn envelope(&self) -> Value {
fn envelope(&self) -> Result<Value, JmapError> {
let method_calls: Vec<Value> = self
.calls
.iter()
.map(|c| json!([c.name, c.args, c.call_id]))
.collect();
json!({ "using": self.using(), "methodCalls": method_calls })
Ok(json!({ "using": self.using()?, "methodCalls": method_calls }))
}
pub fn fits(&self, limits: &Limits) -> Result<(), JmapError> {
if self.calls.len() as u64 > limits.max_calls_in_request {
return Err(JmapError::RequestTooLarge);
}
let size = serde_json::to_vec(&self.envelope())?.len() as u64;
let size = serde_json::to_vec(&self.envelope()?)?.len() as u64;
if size > limits.max_size_request {
return Err(JmapError::RequestTooLarge);
}
@@ -101,7 +128,7 @@ impl Request {
}
pub fn send(&self, client: &HttpClient, api_url: &str) -> Result<Response, JmapError> {
let value = client.post_json(api_url, &self.envelope())?;
let value = client.post_json(api_url, &self.envelope()?)?;
Response::parse(value)
}
}
@@ -855,24 +882,65 @@ mod tests {
let mut r = Request::new();
r.call("Mailbox/get", json!({}), "a");
r.call("Email/query", json!({}), "b");
let u = r.using();
let u = r.using().unwrap();
assert!(u.contains(&URN_CORE.to_owned()));
assert!(u.contains(&"urn:ietf:params:jmap:mail".to_owned()));
assert_eq!(u.iter().filter(|x| x.as_str() == URN_CORE).count(), 1);
}
#[test]
fn using_maps_principal_and_stalwart() {
fn using_maps_standard_types() {
assert_eq!(
using_urn("Principal/query"),
using_urn("Principal/query", None).unwrap(),
"urn:ietf:params:jmap:principals"
);
assert_eq!(using_urn("x:Account/set"), "urn:stalwart:jmap");
assert_eq!(
using_urn("CalendarEvent/set"),
using_urn("CalendarEvent/set", None).unwrap(),
"urn:ietf:params:jmap:calendars"
);
assert_eq!(using_urn("Identity/get"), "urn:ietf:params:jmap:submission");
assert_eq!(
using_urn("Identity/get", None).unwrap(),
"urn:ietf:params:jmap:submission"
);
}
#[test]
fn registry_calls_use_the_inbuxa_capability() {
let mut r = Request::new();
r.registry(Some(URN_INBUXA_REGISTRY));
r.call("x:Account/set", json!({}), "a");
r.call("x:Domain/get", json!({}), "b");
assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_INBUXA_REGISTRY]);
}
#[test]
fn registry_calls_use_the_stalwart_capability_from_a_stalwart_source() {
let mut r = Request::new();
r.registry(Some(URN_STALWART_REGISTRY));
r.call("x:Account/get", json!({}), "a");
assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_STALWART_REGISTRY]);
}
#[test]
fn registry_calls_without_a_registry_capability_fail_clearly() {
let mut r = Request::new();
r.call("Mailbox/get", json!({}), "a");
r.call("x:Domain/set", json!({}), "b");
let err = r.using().unwrap_err();
let msg = err.to_string();
assert!(matches!(err, JmapError::MissingCapability(_)));
assert!(msg.contains("x:Domain/set"), "{msg}");
assert!(
msg.contains(URN_INBUXA_REGISTRY) && msg.contains(URN_STALWART_REGISTRY),
"{msg}"
);
}
#[test]
fn requests_without_registry_calls_need_no_registry() {
let mut r = Request::new();
r.call("Email/get", json!({}), "a");
assert!(r.using().is_ok());
}
#[test]