Shared mailboxes: a second kind of account lock
A shared mailbox (support@, legal@) belongs to no one person: nobody
signs in to it, and the people assigned open it beside their own mail
at an access level an administrator chose. An account lock already is
most of that: it keeps receiving mail, refuses every sign-in, and its
delegates reach it through real grants on every container (so IMAP,
DAV and JMAP honor them), never including Share. So a shared mailbox is
a lock of a second kind (multi-account spec, MA-S; John, 2026-10-05).
Lock gains kind: "lock" (the default, so stored locks read as before)
or "sharedMailbox", set on create and fixed after. A shared mailbox:
- needs no reason to make, change or end;
- holds up to 100 people, where a lock holds 10;
- runs its own Sieve replies and redirects, so an automatic
acknowledgement goes out (a lock answers no one);
- records only what is sent as it (audit_send_as, which now covers it),
not AL-9's access and per-change records, which would bury the log
for a busy desk;
- sends only as itself (MA-S3): From and Reply-To must be its own
addresses, so answers come back to the mailbox and not to whoever
replied; anything else is forbiddenFrom.
The session marks it delegation: {locked: true, kind: "sharedMailbox"},
so a front end that knows no kind still treats it as a lock. The
console's layout gains Management › Directory › Shared Mailboxes
(CustomComponent/SharedMailboxes).
Tests: the account lock suite now goes on to a shared mailbox: made
without a reason with twelve people, sign-in refused, the session's
kind, its vacation reply delivered, an answer sent as it and recorded
as the agent with no per-change records, and a Reply-To naming the
agent refused; a lock unit test reads a stored lock without a kind.
account_lock_tests, jmap_tests, audit_log_tests and imap_tests pass
(RocksDB).
This commit is contained in:
1 parent
5f6548bfdd
commit
9976d52e29
14 files changed
+382
-36
No files matched your search
@@ -446,9 +446,10 @@ impl Server {
|
||||
}
|
||||
|
||||
/// MA-D0a: a message sent from an address that isn't the sender's own:
|
||||
/// a group's, today. The message itself only says `From:` the group, so
|
||||
/// the audit log is where the person who sent it is named. A delegate's
|
||||
/// send is AL-9's record, not this one.
|
||||
/// a group's or a shared mailbox's. The message itself only says
|
||||
/// `From:` that address, so the audit log is where the person who sent
|
||||
/// it is named. A locked account's delegate's send is AL-9's record, not
|
||||
/// this one.
|
||||
pub async fn audit_send_as(
|
||||
&self,
|
||||
token: &AccessToken,
|
||||
@@ -459,7 +460,11 @@ impl Server {
|
||||
let Ok(Some(as_account_id)) = self.account_id_from_email(address, true).await else {
|
||||
return;
|
||||
};
|
||||
if as_account_id == token.account_id() || token.delegation(as_account_id).is_some() {
|
||||
if as_account_id == token.account_id()
|
||||
|| token
|
||||
.delegation(as_account_id)
|
||||
.is_some_and(|delegation| delegation.kind.is_lock())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let actor = self.audit_actor(token).await;
|
||||
|
||||
@@ -46,19 +46,22 @@ impl Server {
|
||||
// inbuxa: AL-2, AL-5: whether this account is locked, and which
|
||||
// locked accounts are handed to it. The token is their cache: every
|
||||
// change to a lock invalidates the tokens it touches.
|
||||
let locked = inbuxa_features::lock::get(self.store(), account_id)
|
||||
let lock_kind = inbuxa_features::lock::get(self.store(), account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.is_some();
|
||||
.map(|lock| lock.kind);
|
||||
let locked = lock_kind.is_some();
|
||||
let shared_mailbox = lock_kind == Some(inbuxa_features::lock::Kind::SharedMailbox);
|
||||
let now_secs = now();
|
||||
let delegations: Box<[super::Delegation]> =
|
||||
inbuxa_features::lock::delegated_to(self.store(), account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into_iter()
|
||||
.filter(|(_, delegate)| delegate.is_current(now_secs))
|
||||
.map(|(locked_id, delegate)| super::Delegation {
|
||||
.filter(|(_, delegate, _)| delegate.is_current(now_secs))
|
||||
.map(|(locked_id, delegate, kind)| super::Delegation {
|
||||
account_id: locked_id,
|
||||
kind,
|
||||
access: delegate.access,
|
||||
send_as: delegate.send_as,
|
||||
until: delegate.until,
|
||||
@@ -247,6 +250,7 @@ impl Server {
|
||||
.map(ConcurrencyLimiter::new),
|
||||
obj_size: 0,
|
||||
locked,
|
||||
shared_mailbox,
|
||||
delegations: delegations.clone(),
|
||||
revision,
|
||||
revision_account,
|
||||
@@ -300,6 +304,7 @@ impl Server {
|
||||
.map(ConcurrencyLimiter::new),
|
||||
obj_size: 0,
|
||||
locked,
|
||||
shared_mailbox,
|
||||
delegations: delegations.clone(),
|
||||
revision,
|
||||
revision_account,
|
||||
@@ -658,6 +663,7 @@ impl AccessToken {
|
||||
credential_version: old_inner.credential_version,
|
||||
obj_size: old_inner.obj_size,
|
||||
locked: old_inner.locked,
|
||||
shared_mailbox: old_inner.shared_mailbox,
|
||||
delegations: old_inner.delegations.clone(),
|
||||
};
|
||||
|
||||
@@ -848,6 +854,18 @@ impl AccessToken {
|
||||
self.inner.locked
|
||||
}
|
||||
|
||||
/// inbuxa: MA-S: the account is a shared mailbox (a lock of that kind).
|
||||
pub fn is_shared_mailbox(&self) -> bool {
|
||||
self.inner.shared_mailbox
|
||||
}
|
||||
|
||||
/// inbuxa: MA-S: this account's delegation into `account_id` is to a
|
||||
/// shared mailbox, not a locked account.
|
||||
pub fn delegated_shared_mailbox(&self, account_id: u32) -> bool {
|
||||
self.delegation(account_id)
|
||||
.is_some_and(|d| d.kind == inbuxa_features::lock::Kind::SharedMailbox)
|
||||
}
|
||||
|
||||
/// inbuxa: AL-5: this account's delegation into a locked account, if it
|
||||
/// has one that hasn't ended.
|
||||
/// inbuxa: AL-6, AL-7: a delegate at organize or full, who may add to
|
||||
@@ -928,6 +946,7 @@ impl AccessToken {
|
||||
credential_version: Default::default(),
|
||||
obj_size: Default::default(),
|
||||
locked: false,
|
||||
shared_mailbox: false,
|
||||
delegations: Default::default(),
|
||||
}),
|
||||
}
|
||||
@@ -988,6 +1007,7 @@ impl AccessTokenInner {
|
||||
credential_version: Default::default(),
|
||||
obj_size: Default::default(),
|
||||
locked: false,
|
||||
shared_mailbox: false,
|
||||
delegations: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,8 @@ pub struct AccessTokenInner {
|
||||
pub(crate) obj_size: u64,
|
||||
// inbuxa: AL-2: the account is locked; it may not authenticate
|
||||
pub(crate) locked: bool,
|
||||
// inbuxa: MA-S: the lock is a shared mailbox
|
||||
pub(crate) shared_mailbox: bool,
|
||||
// inbuxa: AL-5: locked accounts handed to this one
|
||||
pub(crate) delegations: Box<[Delegation]>,
|
||||
}
|
||||
@@ -165,6 +167,8 @@ pub struct Delegation {
|
||||
pub send_as: bool,
|
||||
/// Seconds since the epoch.
|
||||
pub until: Option<u64>,
|
||||
/// MA-S: a locked account, or a shared mailbox.
|
||||
pub kind: inbuxa_features::lock::Kind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Hash, Clone)]
|
||||
|
||||
Reference in new issue
Block a user