Files
inbuxa-server/crates/jmap-proto/src/references/eval.rs
T
jcoffey-dev daa486f7e7
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 8m0s
Journaling: search, read and export over JMAP, and the chain check
Phase 4 of the journaling spec.

- inbuxa:JournalEntry/query and /get (sysJournalSearch): filter by time,
  sender, recipient, either, direction, subject words, Message-ID and
  journal, newest first; the whole report only when asked for.
- inbuxa:JournalExport/set (sysJournalExport): a reason is required; a
  ZIP of the matching reports with manifest.csv, exceptions.csv and
  manifest.sha256, up to 10,000 reports and 1 GB.
- inbuxa:JournalVerification/set (sysJournalGet): chains and reports
  rechecked.
- Every search, listing, read, export and check is written to the audit
  log before anything is returned, with existing actions only.
- Catalog entries for the three objects; spec as-built notes.

journal_tests: administrators can't search; a Compliance Officer searches,
lists, reads a report, exports (reason required) and checks the chain;
the officer can't change journals; each of those is in the audit log.
2026-09-28 21:45:09 -07:00

344 lines
15 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::{
object::{AnyId, JmapObjectId},
references::{
Graph,
jsptr::{EvalResults, ResponsePtr},
},
request::reference::ResultReference,
response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod},
};
use compact_str::format_compact;
use jmap_tools::{Element, Key, Property, Value};
use types::{blob::BlobId, id::Id};
impl Response<'_> {
pub(crate) fn eval_result_references(&self, rr: &ResultReference) -> trc::Result<EvalResults> {
let mut results = EvalResults::default();
for response in &self.method_responses {
if response.id == rr.result_of && response.name == rr.name {
let path = rr.path.iter();
let success = match &response.method {
ResponseMethod::Get(response) => match response {
GetResponseMethod::Email(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Mailbox(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Thread(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Identity(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::EmailSubmission(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::PushSubscription(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Sieve(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::VacationResponse(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::MaskedEmail(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::DeletedAccount(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::AiLimits(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::LogSettings(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::DlpSettings(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::DataInventory(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::InventorySnapshot(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::AuditEvent(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::AuditSettings(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::AccountLock(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::LegalHold(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::MailRule(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::SecurityAcceptance(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Journal(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::JournalEntry(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::HeldMessage(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::HoldExport(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::ProtocolPolicy(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::TenantProtocolPolicy(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Principal(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Quota(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Blob(response) => response.eval_jptr(path, &mut results),
GetResponseMethod::AddressBook(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::ContactCard(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::FileNode(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Calendar(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::CalendarEvent(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::CalendarEventNotification(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::ParticipantIdentity(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::ShareNotification(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::PrincipalAvailability(response) => {
response.eval_jptr(path, &mut results)
}
GetResponseMethod::Registry(response) => {
response.eval_jptr(path, &mut results)
}
},
ResponseMethod::Changes(response) => match response {
ChangesResponseMethod::Email(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::Mailbox(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::Thread(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::Identity(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::EmailSubmission(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::Quota(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::AddressBook(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::ContactCard(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::FileNode(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::Calendar(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::CalendarEvent(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::CalendarEventNotification(response) => {
response.eval_jptr(path, &mut results)
}
ChangesResponseMethod::ShareNotification(response) => {
response.eval_jptr(path, &mut results)
}
// inbuxa: x:MaskedEmail/changes
ChangesResponseMethod::Registry(response) => {
response.eval_jptr(path, &mut results)
}
},
ResponseMethod::Query(response) => response.eval_jptr(path, &mut results),
ResponseMethod::QueryChanges(response) => {
response.eval_jptr(path, &mut results)
}
_ => false,
};
if success {
return Ok(results);
}
}
}
Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!(
"Result reference to {}#{} not found.",
rr.result_of,
rr.name
)))
}
pub(crate) fn eval_id_reference(&self, ir: &str) -> trc::Result<Id> {
if let Some(AnyId::Id(id)) = self.created_ids.get(ir) {
Ok(*id)
} else {
Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!("Id reference {ir:?} not found.")))
}
}
pub(crate) fn eval_blob_id_reference(&self, ir: &str) -> trc::Result<BlobId> {
if let Some(AnyId::BlobId(id)) = self.created_ids.get(ir) {
Ok(id.clone())
} else {
Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!("blobId reference {ir:?} not found.")))
}
}
}
pub(crate) trait EvalObjectReferences {
fn eval_object_references(
&mut self,
response: &Response<'_>,
graph: &mut Graph<'_>,
depth: usize,
max_depth: usize,
eval_strings: bool,
) -> trc::Result<()>;
}
impl<'x, P, E> EvalObjectReferences for Value<'x, P, E>
where
P: Property + JmapObjectId,
E: Element<Property = P> + JmapObjectId,
{
fn eval_object_references(
&mut self,
response: &Response<'_>,
graph: &mut Graph<'_>,
depth: usize,
max_depth: usize,
eval_strings: bool,
) -> trc::Result<()> {
match self {
Value::Element(element) => {
if let Some(id_ref) = element.as_id_ref() {
if let Some(id) = response.created_ids.get(id_ref) {
if !element.try_set_id(id.clone()) {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details("Id reference points to invalid type."));
}
} else if let Graph::Some { child_id, graph } = graph {
graph
.entry(child_id.to_string())
.or_insert_with(Vec::new)
.push(id_ref.to_string());
} else {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!("Id reference {id_ref:?} not found.")));
}
}
}
Value::Array(items) if depth < max_depth => {
// Resolve references in arrays (e.g. emailIds: [#idRef1, #idRef2])
for item in items {
item.eval_object_references(
response,
graph,
depth + 1,
max_depth,
eval_strings,
)?;
}
}
Value::Object(items) if depth < max_depth => {
// Resolve references in JMAP sets (e.g. mailboxIds: { "#idRef1": true, "#idRef2": true })
for (key, value) in items.as_mut_vec() {
if let Key::Property(property) = key
&& let Some(id_ref) = property.as_id_ref()
{
if let Some(id) = response.created_ids.get(id_ref) {
if !property.try_set_id(id.clone()) {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details("Id reference points to invalid type."));
}
} else {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!("Id reference {id_ref:?} not found.")));
}
} else if eval_strings
&& let Some(id) = key
.as_string_key()
.and_then(|k| k.strip_prefix('#'))
.and_then(|id_ref| response.created_ids.get(id_ref))
{
*key = Key::Owned(match id {
AnyId::Id(id) => id.to_string(),
AnyId::BlobId(id) => id.to_string(),
});
}
if matches!(
value,
Value::Element(_) | Value::Array(_) | Value::Object(_)
) {
value.eval_object_references(
response,
graph,
depth + 1,
max_depth,
eval_strings,
)?;
}
}
}
_ => {}
}
Ok(())
}
}