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
@@ -58,6 +58,7 @@ pub trait EmailSubmissionSet: Sync + Send {
|
||||
fn send_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
own_addresses_only: bool,
|
||||
response: &SetResponse<email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
object: Value<'_, EmailSubmissionProperty, EmailSubmissionValue>,
|
||||
@@ -83,7 +84,14 @@ impl EmailSubmissionSet for Server {
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (id, object) in request.unwrap_create() {
|
||||
match self
|
||||
.send_message(account_id, &response, instance, object)
|
||||
.send_message(
|
||||
account_id,
|
||||
// inbuxa: MA-S3: a shared mailbox's people send only as it
|
||||
access_token.delegated_shared_mailbox(account_id),
|
||||
&response,
|
||||
instance,
|
||||
object,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(submission) => {
|
||||
@@ -400,6 +408,7 @@ impl EmailSubmissionSet for Server {
|
||||
async fn send_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
own_addresses_only: bool,
|
||||
response: &SetResponse<email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
object: Value<'_, EmailSubmissionProperty, EmailSubmissionValue>,
|
||||
@@ -629,6 +638,46 @@ impl EmailSubmissionSet for Server {
|
||||
.unarchive::<MessageMetadata>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// inbuxa: MA-S3: mail that came to a shared mailbox goes out as it,
|
||||
// so the answer comes back to the mailbox and not to whoever sent
|
||||
// it: every From and Reply-To address must be the mailbox's own
|
||||
if own_addresses_only {
|
||||
let mut named = Vec::new();
|
||||
for header in metadata.contents[0].parts[0].headers.iter() {
|
||||
if !matches!(
|
||||
header.name,
|
||||
ArchivedMetadataHeaderName::From | ArchivedMetadataHeaderName::ReplyTo
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
match &header.value {
|
||||
ArchivedMetadataHeaderValue::AddressList(addr) => {
|
||||
named.extend(addr.iter().filter_map(|a| a.address.as_ref().map(|v| v.to_string())));
|
||||
}
|
||||
ArchivedMetadataHeaderValue::AddressGroup(groups) => {
|
||||
for group in groups.iter() {
|
||||
named.extend(
|
||||
group
|
||||
.addresses
|
||||
.iter()
|
||||
.filter_map(|a| a.address.as_ref().map(|v| v.to_string())),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for address in named {
|
||||
if self.account_id_from_email(&address, true).await? != Some(account_id) {
|
||||
return Ok(Err(SetError::new(SetErrorType::ForbiddenFrom).with_description(
|
||||
format!(
|
||||
"A shared mailbox sends only as its own addresses, so replies come back to it; {address} isn't one."
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add recipients to envelope if missing
|
||||
let mut bcc_header = None;
|
||||
if rcpt_to.is_empty() {
|
||||
|
||||
Reference in new issue
Block a user