Files
inbuxa-server/crates/jmap-proto/src/references/resolve.rs
T
jcoffey-dev 447229f871
ci / fork-checks (pull_request) Successful in 1m4s
ci / build (pull_request) Successful in 8m47s
Lock accounts: keep receiving mail, no sign-in, hand to delegates
A locked account can't sign in (it fails as a wrong password does), its
sessions end on every node, refresh tokens stop working, and its Sieve
scripts forward and reply to nothing. Mail keeps arriving.

Delegates get real ACL grants on the account's mailboxes, calendars,
address books and files at read, organize or full, with the rights they
replaced restored on unlock. Folders made later are granted after the
create and in a daily sweep. Organize delegates can't destroy; send-as
needs organize or full. The JMAP session marks delegated accounts in
urn:inbuxa:jmap.

New inbuxa:AccountLock object with get/set, permissions 665-668, and a
Compliance > Locked Accounts entry in the schema. Lock, unlock and
delegate changes need a reason and are audited; delegate access and
writes are audited too (audit-hold-lock spec AL-1 to AL-12).
2026-09-27 14:46:06 -07:00

476 lines
19 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::{
error::set::SetError,
method::{
copy::CopyRequest,
get::GetRequest,
import::ImportEmailRequest,
parse::ParseRequest,
search_snippet::GetSearchSnippetRequest,
set::{SetRequest, SetResponse},
upload::{BlobUploadRequest, DataSourceObject},
},
object::{AnyId, JmapObject, JmapObjectId},
references::{Graph, eval::EvalObjectReferences, topological_sort},
request::{
CopyRequestMethod, GetRequestMethod, MaybeInvalid, ParseRequestMethod, RequestMethod,
SetRequestMethod,
reference::{MaybeIdReference, MaybeResultReference},
},
response::Response,
};
use compact_str::format_compact;
use jmap_tools::{Element, Key, Property, Value};
use std::collections::HashMap;
use types::id::Id;
impl Response<'_> {
pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> {
match request {
RequestMethod::Get(request) => match request {
GetRequestMethod::Email(request) => request.resolve_references(self)?,
GetRequestMethod::Mailbox(request) => request.resolve_references(self)?,
GetRequestMethod::Thread(request) => request.resolve_references(self)?,
GetRequestMethod::Identity(request) => request.resolve_references(self)?,
GetRequestMethod::EmailSubmission(request) => request.resolve_references(self)?,
GetRequestMethod::PushSubscription(request) => request.resolve_references(self)?,
GetRequestMethod::Sieve(request) => request.resolve_references(self)?,
GetRequestMethod::VacationResponse(request) => request.resolve_references(self)?,
GetRequestMethod::MaskedEmail(request) => request.resolve_references(self)?,
GetRequestMethod::DeletedAccount(request) => request.resolve_references(self)?,
GetRequestMethod::AiLimits(request) => request.resolve_references(self)?,
GetRequestMethod::AuditEvent(request) => request.resolve_references(self)?,
GetRequestMethod::AuditSettings(request) => request.resolve_references(self)?,
GetRequestMethod::AccountLock(request) => request.resolve_references(self)?,
GetRequestMethod::ProtocolPolicy(request) => request.resolve_references(self)?,
GetRequestMethod::TenantProtocolPolicy(request) => {
request.resolve_references(self)?
}
GetRequestMethod::Principal(request) => request.resolve_references(self)?,
GetRequestMethod::Quota(request) => request.resolve_references(self)?,
GetRequestMethod::Blob(request) => request.resolve_references(self)?,
GetRequestMethod::AddressBook(request) => request.resolve_references(self)?,
GetRequestMethod::ContactCard(request) => request.resolve_references(self)?,
GetRequestMethod::FileNode(request) => request.resolve_references(self)?,
GetRequestMethod::ShareNotification(request) => request.resolve_references(self)?,
GetRequestMethod::Calendar(request) => request.resolve_references(self)?,
GetRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
GetRequestMethod::CalendarEventNotification(request) => {
request.resolve_references(self)?
}
GetRequestMethod::ParticipantIdentity(request) => {
request.resolve_references(self)?
}
GetRequestMethod::PrincipalAvailability(_) => (),
GetRequestMethod::Registry(request) => request.resolve_references(self)?,
},
RequestMethod::Set(request) => match request {
SetRequestMethod::Email(request) => request.resolve_references(self, 2, false)?,
SetRequestMethod::Mailbox(request) => request.resolve_references(self, 1, false)?,
SetRequestMethod::Identity(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::EmailSubmission(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::PushSubscription(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::Sieve(request) => request.resolve_references(self, 1, false)?,
SetRequestMethod::VacationResponse(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::MaskedEmail(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::DeletedAccount(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AiLimits(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::Explanation(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AuditSettings(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AuditExport(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AuditVerification(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AccountLock(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::ProtocolPolicy(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::TenantProtocolPolicy(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::AddressBook(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::ContactCard(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::FileNode(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::ShareNotification(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::Calendar(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::CalendarEvent(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::CalendarEventNotification(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::ParticipantIdentity(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::Registry(request) => request.resolve_references(self, 5, true)?,
},
RequestMethod::Copy(request) => match request {
CopyRequestMethod::Email(request) => request.resolve_references(self, 1, false)?,
CopyRequestMethod::CalendarEvent(request) => {
request.resolve_references(self, 1, false)?
}
CopyRequestMethod::ContactCard(request) => {
request.resolve_references(self, 1, false)?
}
CopyRequestMethod::FileNode(request) => {
request.resolve_references(self, 1, false)?
}
CopyRequestMethod::Blob(_) => (),
},
RequestMethod::ImportEmail(request) => request.resolve_references(self)?,
RequestMethod::SearchSnippet(request) => request.resolve_references(self)?,
RequestMethod::UploadBlob(request) => request.resolve_references(self)?,
RequestMethod::Parse(request) => match request {
ParseRequestMethod::Email(request) => request.resolve_references(self)?,
ParseRequestMethod::ContactCard(request) => request.resolve_references(self)?,
ParseRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
},
_ => {}
}
Ok(())
}
}
pub trait ResolveCreatedReference<P, E>
where
P: Property,
E: Element<Property = P> + JmapObjectId,
{
fn get_created_id(&self, id_ref: &str) -> Option<AnyId>;
fn resolve_self_references(
&self,
value: &mut Value<'_, P, E>,
depth: usize,
eval_strings: bool,
) -> Result<(), SetError<P>> {
match value {
Value::Object(obj) if eval_strings && depth < 5 => {
for (key, value) in obj.as_mut_vec() {
if let Some(id) = key
.as_string_key()
.and_then(|k| k.strip_prefix('#'))
.and_then(|id_ref| self.get_created_id(id_ref))
{
*key = Key::Owned(match id {
AnyId::Id(id) => id.to_string(),
AnyId::BlobId(id) => id.to_string(),
});
}
if matches!(value, Value::Object(_) | Value::Array(_)) {
self.resolve_self_references(value, depth + 1, eval_strings)?;
}
}
}
Value::Element(element) => {
if let Some(id_ref) = element.as_id_ref() {
if let Some(id) = self.get_created_id(id_ref) {
if !element.try_set_id(id) {
return Err(SetError::invalid_properties()
.with_description("Id reference points to invalid type."));
}
} else {
return Err(SetError::not_found()
.with_description(format!("Id reference {id_ref:?} not found.")));
}
}
}
Value::Array(items) if depth < 5 => {
for item in items {
self.resolve_self_references(item, depth + 1, eval_strings)?;
}
}
_ => {}
}
Ok(())
}
}
pub(crate) trait ResolveReference {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()>;
}
pub(crate) trait ResolveSetReference {
fn resolve_references(
&mut self,
response: &Response<'_>,
max_depth: usize,
eval_strings: bool,
) -> trc::Result<()>;
}
impl<T: JmapObject> ResolveReference for GetRequest<T> {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
// Resolve id references
match &mut self.ids {
Some(MaybeResultReference::Reference(reference)) => {
self.ids = Some(MaybeResultReference::Value(
response
.eval_result_references(reference)?
.into_ids::<T::Id>(reference)
.map(|f| f.map(MaybeIdReference::Id))
.collect::<Result<_, _>>()?,
));
}
Some(MaybeResultReference::Value(ids)) => {
for id in ids {
if let MaybeIdReference::Reference(reference) = id {
if let Some(resolved_id) = response
.created_ids
.get(reference)
.cloned()
.and_then(|v| T::Id::try_from(v).ok())
{
*id = MaybeIdReference::Id(resolved_id);
} else {
return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
format_compact!(
"Id reference {reference:?} does not exist or is invalid."
),
));
}
}
}
}
_ => (),
}
// Resolve properties references
if let Some(MaybeResultReference::Reference(reference)) = &self.properties {
self.properties = Some(MaybeResultReference::Value(
response
.eval_result_references(reference)?
.into_properties::<T::Property>(reference)
.map(|f| f.map(MaybeInvalid::Value))
.collect::<Result<_, _>>()?,
));
}
Ok(())
}
}
impl<'x, T: JmapObject> ResolveSetReference for SetRequest<'x, T> {
fn resolve_references(
&mut self,
response: &Response<'_>,
max_depth: usize,
eval_strings: bool,
) -> trc::Result<()> {
// Resolve create references
if let Some(create) = &mut self.create {
let mut graph = HashMap::with_capacity(create.len());
for (id, obj) in create.iter_mut() {
obj.eval_object_references(
response,
&mut Graph::Some {
child_id: &*id,
graph: &mut graph,
},
0,
max_depth,
eval_strings,
)?;
}
// Perform topological sort
if !graph.is_empty() {
self.create = topological_sort(create, graph)?.into();
}
}
// Resolve update references
if let Some(update) = &mut self.update {
for obj in update.values_mut() {
obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;
}
}
// Resolve destroy references
if let Some(MaybeResultReference::Reference(reference)) = &self.destroy {
self.destroy = Some(MaybeResultReference::Value(
response
.eval_result_references(reference)?
.into_ids::<Id>(reference)
.map(|f| f.map(MaybeInvalid::Value))
.collect::<Result<_, _>>()?,
));
}
Ok(())
}
}
impl<'x, T: JmapObject> ResolveSetReference for CopyRequest<'x, T> {
fn resolve_references(
&mut self,
response: &Response<'_>,
max_depth: usize,
eval_strings: bool,
) -> trc::Result<()> {
// Resolve create references
for (id, obj) in self.create.iter_mut() {
obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;
if let MaybeIdReference::Reference(ir) = id {
*id = MaybeIdReference::Id(response.eval_id_reference(ir)?);
}
}
Ok(())
}
}
impl<T: JmapObject> ResolveReference for ParseRequest<T> {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
// Resolve blobId references
for id in self.blob_ids.iter_mut() {
if let MaybeIdReference::Reference(ir) = id {
*id = MaybeIdReference::Id(response.eval_blob_id_reference(ir)?);
}
}
Ok(())
}
}
impl ResolveReference for ImportEmailRequest {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
// Resolve email mailbox references
for email in self.emails.values_mut() {
match &mut email.mailbox_ids {
MaybeResultReference::Reference(reference) => {
email.mailbox_ids = MaybeResultReference::Value(
response
.eval_result_references(reference)?
.into_ids::<Id>(reference)
.map(|f| f.map(MaybeIdReference::Id))
.collect::<Result<_, _>>()?,
);
}
MaybeResultReference::Value(values) => {
for value in values {
if let MaybeIdReference::Reference(ir) = value {
*value = MaybeIdReference::Id(response.eval_id_reference(ir)?);
}
}
}
}
}
Ok(())
}
}
impl ResolveReference for GetSearchSnippetRequest {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
// Resolve emailIds references
if let MaybeResultReference::Reference(reference) = &self.email_ids {
self.email_ids = MaybeResultReference::Value(
response
.eval_result_references(reference)?
.into_ids::<Id>(reference)
.map(|f| f.map(MaybeInvalid::Value))
.collect::<Result<_, _>>()?,
);
}
Ok(())
}
}
impl ResolveReference for BlobUploadRequest {
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
let mut graph = HashMap::with_capacity(self.create.len());
for (create_id, object) in self.create.iter_mut() {
for data in &mut object.data {
if let DataSourceObject::Id { id, .. } = data
&& let MaybeIdReference::Reference(parent_id) = id
{
match response.created_ids.get(parent_id) {
Some(AnyId::BlobId(blob_id)) => {
*id = MaybeIdReference::Id(blob_id.clone());
}
Some(_) => {
return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
format_compact!(
"Id reference {parent_id:?} points to invalid type."
),
));
}
None => {
graph
.entry(create_id.to_string())
.or_insert_with(Vec::new)
.push(parent_id.to_string());
}
}
}
}
}
// Perform topological sort
if !graph.is_empty() {
self.create = topological_sort(&mut self.create, graph)?;
}
Ok(())
}
}
impl<T> ResolveCreatedReference<T::Property, T::Element> for SetResponse<T>
where
T: JmapObject,
{
fn get_created_id(&self, id_ref: &str) -> Option<AnyId> {
self.created
.get(id_ref)
.and_then(|v| v.as_object())
.and_then(|v| v.get(&Key::Property(T::ID_PROPERTY)))
.and_then(|v| v.as_element())
.and_then(|v| v.as_any_id())
}
}