Files
inbuxa-server/crates/jmap-proto/src/object/mod.rs
T
jcoffey-dev fb785b8635
ci / github (pull_request) Skipped
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 4m6s
github/ci (branch) GitHub Actions
Who may share mail: a server switch, and a tenant's that can only be stricter
A school, or any organization that doesn't want people's mailboxes
shared, can now turn that off (multi-account spec, MA-C). Two switches
at two levels, as the legacy-protocols switch has:

- mailSharing: people may share their own mail folders;
- addAccounts: people may add other accounts to the webmail (read by
  the webmail's account switcher, MA-B).

inbuxa:SharingPolicy/get and /set hold them: the server's policy has
the singleton id, each tenant's has the tenant's id. Both default to
on, so nothing changes until someone turns one off. A tenant's
administrator changes their own tenant's (the domain's permissions, as
for its protocols switch); only a server administrator with
sysSharingUpdate changes the server's; a tenant can never be looser
than the server (forbidden). Every change goes through the audit log,
and rebuilds every access token, here and on every node.

With mail sharing off for an account's tenant (or the server):

- Mailbox/set and IMAP SETACL refuse to start or widen a share
  (forbidden / NO [NOPERM]); narrowing or ending one is always allowed;
- shares already made give nothing while it is off: an access token
  leaves out mailbox grants from such an owner. They stay stored, so
  turning sharing back on restores them (John, 2026-10-05);
- a lock's and a shared mailbox's grants are an administrator's and
  always count, and group membership was never a share.

The session's own account says mailSharing and addAccounts, the
stricter of the two levels, so front ends can hide what is off.

Tests: a new sharing_policy suite with a school tenant, its own
administrator and two people outside it: on by default; the school's
administrator turns it off but can't touch the server's; an old share
stops working and a new one is refused while someone outside the school
is unaffected; a shared mailbox in the school keeps working; the server
off can't be loosened by the tenant; on again restores the old share;
ending a share works while off; and every change is audited. A unit
test covers the stricter-only rule. sharing_policy_tests, jmap_tests,
imap_tests, account_lock_tests and audit_log_tests pass (RocksDB).
2026-10-05 16:00:09 -07:00

234 lines
6.4 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::request::deserialize::DeserializeArguments;
use jmap_tools::{Element, Null, Property};
use serde::Serialize;
use std::{fmt::Debug, str::FromStr};
use types::{acl::Acl, blob::BlobId, id::Id};
pub mod addressbook;
pub mod blob;
pub mod calendar;
pub mod calendar_event;
pub mod calendar_event_notification;
pub mod contact;
pub mod email;
pub mod email_submission;
pub mod fastmail_masked_email; // inbuxa: masked email
pub mod inbuxa_account_lock; // inbuxa: account lock with delegation
pub mod inbuxa_ai_limits; // inbuxa: AI spam classification
pub mod inbuxa_log_settings; // inbuxa: personal-data catalog, D1
pub mod inbuxa_dlp_settings; // inbuxa: DLP settings
pub mod inbuxa_data_inventory; // inbuxa: personal-data catalog
pub mod inbuxa_inventory_snapshot; // inbuxa: personal-data catalog
pub mod inbuxa_audit; // inbuxa: the audit log
pub mod inbuxa_legal_hold; // inbuxa: legal hold
pub mod inbuxa_mail_rule; // inbuxa: DLP and mail flow rules
pub mod inbuxa_security_acceptance; // inbuxa: accepted security to-do items
pub mod inbuxa_journal; // inbuxa: journaling
pub mod inbuxa_journal_entry; // inbuxa: journaling, search and export
pub mod inbuxa_held_message; // inbuxa: mail held for review
pub mod inbuxa_hold_export; // inbuxa: legal hold exports
pub mod inbuxa_explanation; // inbuxa: "Explain this" with the local model
pub mod inbuxa_protocol_policy; // inbuxa: legacy protocols off
pub mod inbuxa_tenant_protocol_policy; // inbuxa: legacy protocols off, per tenant
pub mod inbuxa_sharing_policy; // inbuxa: MA-C, who may share mail
pub mod inbuxa_deleted_account; // inbuxa: undelete
pub mod file_node;
pub mod identity;
pub mod mailbox;
pub mod participant_identity;
pub mod principal;
pub mod push_subscription;
pub mod quota;
pub mod registry;
pub mod search_snippet;
pub mod share_notification;
pub mod sieve;
pub mod thread;
pub mod vacation_response;
pub trait JmapObject: std::fmt::Debug {
type Property: Property + JmapObjectId + FromStr + Debug + Sync + Send;
type Element: Element<Property = Self::Property> + JmapObjectId + Debug + Sync + Send;
type Id: FromStr + TryFrom<AnyId> + Into<Self::Element> + Serialize + Debug + Sync + Send;
type Filter: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
type GetArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
type SetArguments<'de>: Default + DeserializeArguments<'de> + Debug + Sync + Send;
type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
type ParseArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
const ID_PROPERTY: Self::Property;
}
pub trait JmapSharedObject: JmapObject {
type Right: JmapRight + Into<Self::Property> + Debug + Clone + Copy + Sync + Send;
const SHARE_WITH_PROPERTY: Self::Property;
}
pub trait JmapRight: Clone + Copy + Sized + 'static {
fn all_rights() -> &'static [Self];
fn to_acl(&self) -> &'static [Acl];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(untagged)]
pub enum AnyId {
Id(Id),
BlobId(BlobId),
}
pub trait JmapObjectId {
fn as_id(&self) -> Option<Id>;
fn as_any_id(&self) -> Option<AnyId>;
fn as_id_ref(&self) -> Option<&str>;
fn try_set_id(&mut self, new_id: AnyId) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MaybeReference<T: FromStr> {
Value(T),
Reference(String),
ParseError,
}
fn parse_ref<T: FromStr>(value: &str) -> MaybeReference<T> {
if let Some(reference) = value.strip_prefix('#') {
MaybeReference::Reference(reference.to_string())
} else {
T::from_str(value)
.map(MaybeReference::Value)
.unwrap_or(MaybeReference::ParseError)
}
}
impl From<Id> for AnyId {
fn from(value: Id) -> Self {
AnyId::Id(value)
}
}
impl From<BlobId> for AnyId {
fn from(value: BlobId) -> Self {
AnyId::BlobId(value)
}
}
impl TryFrom<AnyId> for Id {
type Error = ();
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
if let AnyId::Id(id) = value {
Ok(id)
} else {
Err(())
}
}
}
impl TryFrom<AnyId> for BlobId {
type Error = ();
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
if let AnyId::BlobId(id) = value {
Ok(id)
} else {
Err(())
}
}
}
impl<'de> serde::Deserialize<'de> for AnyId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <&str>::deserialize(deserializer)?;
if let Some(blob_id) = BlobId::from_base32(value) {
Ok(AnyId::BlobId(blob_id))
} else if let Ok(id) = Id::from_str(value) {
Ok(AnyId::Id(id))
} else {
Err(serde::de::Error::custom(format!(
"Invalid AnyId: {}",
value
)))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
pub struct NullObject;
impl JmapObject for NullObject {
type Property = Null;
type Element = Null;
type Id = Null;
type Filter = ();
type Comparator = ();
type GetArguments = ();
type SetArguments<'de> = ();
type QueryArguments = ();
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = Null;
}
impl JmapRight for Null {
fn all_rights() -> &'static [Self] {
unreachable!()
}
fn to_acl(&self) -> &'static [Acl] {
unreachable!()
}
}
impl FromStr for NullObject {
type Err = ();
fn from_str(_: &str) -> Result<Self, Self::Err> {
unreachable!()
}
}
impl JmapObjectId for Null {
fn as_id(&self) -> Option<Id> {
unreachable!()
}
fn as_any_id(&self) -> Option<AnyId> {
unreachable!()
}
fn as_id_ref(&self) -> Option<&str> {
unreachable!()
}
fn try_set_id(&mut self, _: AnyId) -> bool {
unreachable!()
}
}
impl TryFrom<AnyId> for Null {
type Error = ();
fn try_from(_: AnyId) -> Result<Self, Self::Error> {
unreachable!()
}
}