Files
inbuxa-server/crates/jmap-proto/src/references/resolve.rs
T
jcoffey-dev a24ed3b60a
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m6s
Deliverability check: each node asks what the internet sees of it
Deliverability spec (inbuxa-drafts specs/deliverability.md), the server
side. Every node that sends mail checks itself once a day, at its own
minute in the first hour (UTC), and when an administrator asks:

- its outgoing addresses (the connection strategy's, or what its EHLO
  name resolves to), their reverse DNS and whether it resolves back,
  and nine blocklists, read by each list's own codes so a refused
  query is never taken for a listing (DL-1 to DL-6);
- for every domain: SPF for each address, each DKIM key (by signing a
  message that's never sent and verifying it as a receiver would),
  DMARC, the MTA-STS policy against the MX, TLS reporting, and the
  domain blocklists (DL-7 to DL-12);
- whether it holds a certificate for its EHLO and MX names (DL-13).

It keeps one report per node, facts only; the console grades them.

- inbuxa:DeliverabilityReport: /get, and a create that asks every node
  to check now, broadcast as DeliverabilityCheck (DL-15). A tenant
  administrator gets their own domains only (DL-20).
- inbuxa:DeliverabilitySettings: which built-in lists are left out, and
  the lists themselves (DL-6).
- sysDeliverabilityGet, sysDeliverabilityUpdate, sysDeliverabilityCheck;
  a tenant ceiling always turns the last two off.
2026-10-05 16:17:33 -07:00

535 lines
23 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::DeliverabilityReport(request) => request.resolve_references(self)?,
GetRequestMethod::DeliverabilitySettings(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::SharingPolicy(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::DeliverabilityReport(request) => {
request.resolve_references(self, 1, false)?
}
SetRequestMethod::DeliverabilitySettings(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::SharingPolicy(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())
}
}