Lock accounts: keep receiving mail, no sign-in, hand to delegates
ci / fork-checks (pull_request) Successful in 1m4s
ci / build (pull_request) Successful in 8m47s

A locked account can't sign in (it fails as a wrong password does), its
sessions end on every node, refresh tokens stop working, and its Sieve
scripts forward and reply to nothing. Mail keeps arriving.

Delegates get real ACL grants on the account's mailboxes, calendars,
address books and files at read, organize or full, with the rights they
replaced restored on unlock. Folders made later are granted after the
create and in a daily sweep. Organize delegates can't destroy; send-as
needs organize or full. The JMAP session marks delegated accounts in
urn:inbuxa:jmap.

New inbuxa:AccountLock object with get/set, permissions 665-668, and a
Compliance > Locked Accounts entry in the schema. Lock, unlock and
delegate changes need a reason and are audited; delegate access and
writes are audited too (audit-hold-lock spec AL-1 to AL-12).
This commit is contained in:
2026-09-27 14:46:06 -07:00
parent ebf2fe11d9
commit 447229f871
46 changed files with 2573 additions and 29 deletions
+12
View File
@@ -2,6 +2,8 @@
* 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 common::ipc::{
@@ -139,6 +141,11 @@ impl BroadcastBatch<Vec<BroadcastEvent>> {
BroadcastEvent::QueueRefresh => {
serialized.push(12u8);
}
// inbuxa: AL-3
BroadcastEvent::EndSessions(account_id) => {
serialized.push(13u8);
let _ = serialized.write_leb128(*account_id);
}
}
}
serialized
@@ -272,6 +279,11 @@ where
10 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: true })),
11 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: false })),
12 => Ok(Some(BroadcastEvent::QueueRefresh)),
// inbuxa: AL-3
13 => {
let account_id = self.messages.next_leb128().ok_or(())?;
Ok(Some(BroadcastEvent::EndSessions(account_id)))
}
_ => Err(()),
}
} else {
@@ -180,6 +180,15 @@ pub fn spawn_broadcast_subscriber(inner: Arc<Inner>, mut shutdown_rx: watch::Rec
.send(QueueEvent::Paused(!is_running))
.await;
}
// inbuxa: AL-3: sessions an account has
// open here end too
BroadcastEvent::EndSessions(account_id) => {
let _ = inner
.ipc
.push_tx
.send(PushEvent::Revoke { account_id })
.await;
}
BroadcastEvent::QueueRefresh => {
if inner.shared_core.load().network.roles.outbound_mta {
let _ = inner
@@ -266,6 +275,9 @@ fn log_event(event: &BroadcastEvent) -> trc::Value {
BroadcastEvent::PushServerUpdate(account_id) => {
trc::Value::Array(vec!["PushServerUpdate".into(), (*account_id).into()])
}
BroadcastEvent::EndSessions(account_id) => {
trc::Value::Array(vec!["EndSessions".into(), (*account_id).into()])
}
BroadcastEvent::RegistryChange(change) => match change {
RegistryChange::Insert(id) => trc::Value::Array(vec![
"RegistryInsert".into(),
@@ -263,6 +263,12 @@ async fn store_maintenance(
}
}
// inbuxa: AL-7: locks' grants reach folders the server made on
// its own (a Sieve fileinto :create)
if let Err(err) = email::inbuxa_lock::reconcile_all(server).await {
trc::error!(err.details("Failed to re-apply account locks"));
}
// inbuxa: AU-7: audit records past their retention go; a
// failure leaves them for the next run
if let Err(err) = server.audit_purge().await {