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.
521 lines
22 KiB
Rust
521 lines
22 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::LogSettings(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::DlpSettings(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::DataInventory(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::InventorySnapshot(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::LegalHold(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::MailRule(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::SecurityAcceptance(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::Journal(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::JournalEntry(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::HeldMessage(request) => request.resolve_references(self)?,
|
|
GetRequestMethod::HoldExport(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::LogSettings(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::DlpSettings(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::LegalHold(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::MailRule(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::SecurityAcceptance(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::Journal(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::JournalExport(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::JournalVerification(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::HeldMessage(request) => {
|
|
request.resolve_references(self, 1, false)?
|
|
}
|
|
SetRequestMethod::HoldExport(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() {
|
|
if let MaybeIdReference::Reference(ir) = &email.blob_id {
|
|
email.blob_id = MaybeIdReference::Id(response.eval_blob_id_reference(ir)?);
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|