/* * 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 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::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::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 where P: Property, E: Element + JmapObjectId, { fn get_created_id(&self, id_ref: &str) -> Option; fn resolve_self_references( &self, value: &mut Value<'_, P, E>, depth: usize, eval_strings: bool, ) -> Result<(), SetError

> { 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 ResolveReference for GetRequest { 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::(reference) .map(|f| f.map(MaybeIdReference::Id)) .collect::>()?, )); } 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::(reference) .map(|f| f.map(MaybeInvalid::Value)) .collect::>()?, )); } 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::(reference) .map(|f| f.map(MaybeInvalid::Value)) .collect::>()?, )); } 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 ResolveReference for ParseRequest { 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::(reference) .map(|f| f.map(MaybeIdReference::Id)) .collect::>()?, ); } 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::(reference) .map(|f| f.map(MaybeInvalid::Value)) .collect::>()?, ); } 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 ResolveCreatedReference for SetResponse where T: JmapObject, { fn get_created_id(&self, id_ref: &str) -> Option { 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()) } }