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).
666 lines
27 KiB
Rust
666 lines
27 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::{
|
|
api::acl::{JmapAcl, JmapRights},
|
|
changes::state::JmapCacheState,
|
|
};
|
|
use common::{
|
|
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
|
|
};
|
|
#[allow(unused_imports)]
|
|
use email::mailbox::{INBOX_ID, JUNK_ID, TRASH_ID, UidMailbox};
|
|
use email::{
|
|
cache::{MessageCacheFetch, mailbox::MailboxCacheAccess},
|
|
mailbox::{
|
|
Mailbox,
|
|
destroy::{MailboxDestroy, MailboxDestroyError},
|
|
},
|
|
};
|
|
use jmap_proto::{
|
|
error::set::{SetError, SetErrorType},
|
|
method::set::{SetRequest, SetResponse},
|
|
object::mailbox::{self, MailboxProperty, MailboxValue},
|
|
references::resolve::ResolveCreatedReference,
|
|
request::MaybeInvalid,
|
|
types::state::State,
|
|
};
|
|
use jmap_tools::{JsonPointerItem, Key, Map, Value};
|
|
use registry::schema::enums::{Permission, StorageQuota};
|
|
use std::future::Future;
|
|
use store::{
|
|
ValueKey,
|
|
roaring::RoaringBitmap,
|
|
write::{AlignedBytes, Archive, BatchBuilder, assert::AssertValue},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::Acl, collection::Collection, field::MailboxField, id::Id, special_use::SpecialUse,
|
|
};
|
|
|
|
pub struct SetContext<'x> {
|
|
account_id: u32,
|
|
access_token: &'x AccessToken,
|
|
is_shared: bool,
|
|
response: SetResponse<mailbox::Mailbox>,
|
|
mailbox_ids: RoaringBitmap,
|
|
will_destroy: Vec<Id>,
|
|
}
|
|
|
|
pub trait MailboxSet: Sync + Send {
|
|
fn mailbox_set(
|
|
&self,
|
|
request: SetRequest<'_, mailbox::Mailbox>,
|
|
access_token: &AccessToken,
|
|
) -> impl Future<Output = trc::Result<SetResponse<mailbox::Mailbox>>> + Send;
|
|
|
|
fn mailbox_set_item(
|
|
&self,
|
|
changes_: Map<'_, MailboxProperty, MailboxValue>,
|
|
update: Option<(u32, Archive<Mailbox>)>,
|
|
ctx: &SetContext,
|
|
) -> impl Future<
|
|
Output = trc::Result<
|
|
Result<ObjectIndexBuilder<Mailbox, Mailbox>, SetError<MailboxProperty>>,
|
|
>,
|
|
> + Send;
|
|
}
|
|
|
|
impl MailboxSet for Server {
|
|
#[allow(clippy::blocks_in_conditions)]
|
|
async fn mailbox_set(
|
|
&self,
|
|
mut request: SetRequest<'_, mailbox::Mailbox>,
|
|
access_token: &AccessToken,
|
|
) -> trc::Result<SetResponse<mailbox::Mailbox>> {
|
|
// Prepare response
|
|
let account_id = request.account_id.document_id();
|
|
let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false);
|
|
let cache = self.get_cached_messages(account_id).await?;
|
|
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
|
|
.with_state(cache.assert_state(true, &request.if_in_state)?);
|
|
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
|
let mut ctx = SetContext {
|
|
account_id,
|
|
is_shared: access_token.is_shared(account_id),
|
|
access_token,
|
|
response,
|
|
mailbox_ids: RoaringBitmap::from_iter(cache.mailboxes.index.keys()),
|
|
will_destroy,
|
|
};
|
|
let mut change_id = None;
|
|
let account_info = self.account(account_id).await?;
|
|
|
|
// Process creates
|
|
let mut batch = BatchBuilder::new();
|
|
'create: for (id, object) in request.unwrap_create() {
|
|
let Some(object) = object.into_object() else {
|
|
continue;
|
|
};
|
|
|
|
// Validate quota
|
|
if ctx.mailbox_ids.len()
|
|
>= self.object_quota(account_info.object_quotas(), StorageQuota::MaxMailboxes)
|
|
as u64
|
|
{
|
|
ctx.response.not_created.append(
|
|
id,
|
|
SetError::new(SetErrorType::OverQuota).with_description(concat!(
|
|
"There are too many mailboxes, ",
|
|
"please delete some before adding a new one."
|
|
)),
|
|
);
|
|
continue 'create;
|
|
}
|
|
|
|
match self.mailbox_set_item(object, None, &ctx).await? {
|
|
Ok(builder) => {
|
|
batch
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::Mailbox);
|
|
|
|
let parent_id = builder.changes().unwrap().parent_id;
|
|
if parent_id > 0 {
|
|
batch
|
|
.with_document(parent_id - 1)
|
|
.assert_value(MailboxField::Archive, AssertValue::Some);
|
|
}
|
|
|
|
let document_id = self
|
|
.store()
|
|
.assign_document_ids(account_id, Collection::Mailbox, 1)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
batch
|
|
.with_document(document_id)
|
|
.custom(builder)
|
|
.caused_by(trc::location!())?
|
|
.commit_point();
|
|
|
|
ctx.mailbox_ids.insert(document_id);
|
|
ctx.response.created(id, document_id);
|
|
}
|
|
Err(err) => {
|
|
ctx.response.not_created.append(id, err);
|
|
continue 'create;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !batch.is_empty() {
|
|
change_id = self
|
|
.commit_batch(batch)
|
|
.await
|
|
.and_then(|ids| ids.last_change_id(account_id))
|
|
.caused_by(trc::location!())?
|
|
.into();
|
|
}
|
|
|
|
// Process updates
|
|
let mut will_update = Vec::with_capacity(request.update.as_ref().map_or(0, |u| u.len()));
|
|
let mut batch = BatchBuilder::new();
|
|
'update: for (id, object) in request.unwrap_update() {
|
|
let id = match id {
|
|
MaybeInvalid::Value(id) => id,
|
|
invalid => {
|
|
ctx.response
|
|
.not_updated
|
|
.append(invalid, SetError::not_found());
|
|
continue 'update;
|
|
}
|
|
};
|
|
// Make sure id won't be destroyed
|
|
if ctx.will_destroy.contains(&id) {
|
|
ctx.response
|
|
.not_updated
|
|
.append(id, SetError::will_destroy());
|
|
continue 'update;
|
|
}
|
|
let Some(object) = object.into_object() else {
|
|
continue 'update;
|
|
};
|
|
|
|
// Obtain mailbox
|
|
let document_id = id.document_id();
|
|
if let Some(mailbox) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::Mailbox,
|
|
document_id,
|
|
))
|
|
.await?
|
|
{
|
|
// Validate ACL
|
|
let mailbox = mailbox
|
|
.into_deserialized::<email::mailbox::Mailbox>()
|
|
.caused_by(trc::location!())?;
|
|
if ctx.is_shared {
|
|
let acl = mailbox.inner.acls.effective_acl(access_token);
|
|
let subscription_only = object.keys().all(|key| {
|
|
matches!(
|
|
key,
|
|
Key::Property(MailboxProperty::IsSubscribed | MailboxProperty::Id)
|
|
)
|
|
});
|
|
if subscription_only {
|
|
if !acl.contains(Acl::Read) {
|
|
ctx.response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(
|
|
"You are not allowed to access this mailbox.",
|
|
),
|
|
);
|
|
continue 'update;
|
|
}
|
|
} else if !acl.contains(Acl::Modify) {
|
|
ctx.response.not_updated.append(
|
|
id,
|
|
SetError::forbidden()
|
|
.with_description("You are not allowed to modify this mailbox."),
|
|
);
|
|
continue 'update;
|
|
} else if object.contains_key(&Key::Property(MailboxProperty::ShareWith))
|
|
&& !acl.contains(Acl::Share)
|
|
{
|
|
ctx.response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(
|
|
"You are not allowed to change the permissions of this mailbox.",
|
|
),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
|
|
match self
|
|
.mailbox_set_item(object, (document_id, mailbox).into(), &ctx)
|
|
.await?
|
|
{
|
|
Ok(builder) => {
|
|
batch
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::Mailbox);
|
|
|
|
let parent_id = builder.changes().unwrap().parent_id;
|
|
if parent_id > 0 {
|
|
batch
|
|
.with_document(parent_id - 1)
|
|
.assert_value(MailboxField::Archive, AssertValue::Some);
|
|
}
|
|
|
|
batch
|
|
.with_document(document_id)
|
|
.custom(builder)
|
|
.caused_by(trc::location!())?
|
|
.commit_point();
|
|
will_update.push(id);
|
|
}
|
|
Err(err) => {
|
|
ctx.response.not_updated.append(id, err);
|
|
continue 'update;
|
|
}
|
|
}
|
|
} else {
|
|
ctx.response.not_updated.append(id, SetError::not_found());
|
|
}
|
|
}
|
|
|
|
if !batch.is_empty() {
|
|
match self
|
|
.commit_batch(batch)
|
|
.await
|
|
.and_then(|ids| ids.last_change_id(account_id))
|
|
{
|
|
Ok(change_id_) => {
|
|
change_id = Some(change_id_);
|
|
for id in will_update {
|
|
ctx.response.updated.append(id, None);
|
|
}
|
|
}
|
|
Err(err) if err.is_assertion_failure() => {
|
|
for id in will_update {
|
|
ctx.response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(
|
|
"Another process modified this mailbox, please try again.",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
return Err(err.caused_by(trc::location!()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process deletions
|
|
for id in ctx.will_destroy {
|
|
match self
|
|
.mailbox_destroy(
|
|
account_id,
|
|
id.document_id(),
|
|
ctx.access_token,
|
|
on_destroy_remove_emails,
|
|
)
|
|
.await?
|
|
{
|
|
Ok(change_id_) => {
|
|
if change_id_.is_some() {
|
|
change_id = change_id_;
|
|
}
|
|
ctx.response.destroyed.push(id);
|
|
}
|
|
Err(err) => {
|
|
ctx.response.not_destroyed.append(
|
|
id,
|
|
match err {
|
|
MailboxDestroyError::CannotDestroy => SetError::forbidden()
|
|
.with_description(
|
|
"You are not allowed to delete Inbox, Junk or Trash folders.",
|
|
),
|
|
MailboxDestroyError::Forbidden => SetError::forbidden()
|
|
.with_description("You are not allowed to delete this mailbox."),
|
|
MailboxDestroyError::HasChildren => {
|
|
SetError::new(SetErrorType::MailboxHasChild)
|
|
.with_description("Mailbox has at least one children.")
|
|
}
|
|
MailboxDestroyError::HasEmails => {
|
|
SetError::new(SetErrorType::MailboxHasEmail)
|
|
.with_description("Mailbox is not empty.")
|
|
}
|
|
MailboxDestroyError::NotFound => SetError::not_found(),
|
|
MailboxDestroyError::AssertionFailed => SetError::forbidden()
|
|
.with_description(concat!(
|
|
"Another process modified a message in this mailbox ",
|
|
"while deleting it, please try again."
|
|
)),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write changes
|
|
if let Some(change_id) = change_id {
|
|
ctx.response.new_state = State::Exact(change_id).into();
|
|
}
|
|
|
|
Ok(ctx.response)
|
|
}
|
|
|
|
#[allow(clippy::blocks_in_conditions)]
|
|
async fn mailbox_set_item(
|
|
&self,
|
|
changes_: Map<'_, MailboxProperty, MailboxValue>,
|
|
update: Option<(u32, Archive<Mailbox>)>,
|
|
ctx: &SetContext<'_>,
|
|
) -> trc::Result<Result<ObjectIndexBuilder<Mailbox, Mailbox>, SetError<MailboxProperty>>> {
|
|
// Parse properties
|
|
let mut changes = update
|
|
.as_ref()
|
|
.map(|(_, obj)| obj.inner.clone())
|
|
.unwrap_or_else(|| Mailbox::new(String::new()));
|
|
let mut has_acl_changes = false;
|
|
for (property, mut value) in changes_.into_vec() {
|
|
if let Err(err) = ctx.response.resolve_self_references(&mut value, 0, false) {
|
|
return Ok(Err(err));
|
|
};
|
|
match (&property, value) {
|
|
(Key::Property(MailboxProperty::Name), Value::Str(value)) => {
|
|
let value = value.trim();
|
|
if !value.is_empty() && value.len() < self.core.email.mailbox_name_max_len {
|
|
changes.name = value.into();
|
|
} else {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::Name)
|
|
.with_description(
|
|
if !value.is_empty() {
|
|
"Mailbox name is too long."
|
|
} else {
|
|
"Mailbox name cannot be empty."
|
|
}
|
|
.to_string(),
|
|
)));
|
|
}
|
|
}
|
|
(
|
|
Key::Property(MailboxProperty::ParentId),
|
|
Value::Element(MailboxValue::Id(value)),
|
|
) => {
|
|
let parent_id = value.document_id();
|
|
if ctx.will_destroy.contains(&value) {
|
|
return Ok(Err(SetError::will_destroy()
|
|
.with_description("Parent ID will be destroyed.")));
|
|
} else if !ctx.mailbox_ids.contains(parent_id) {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_description("Parent ID does not exist.")));
|
|
}
|
|
changes.parent_id = parent_id + 1;
|
|
}
|
|
(Key::Property(MailboxProperty::ParentId), Value::Null) => {
|
|
changes.parent_id = 0;
|
|
}
|
|
(Key::Property(MailboxProperty::IsSubscribed), Value::Bool(subscribe)) => {
|
|
let account_id = ctx
|
|
.access_token
|
|
.personal_id(ctx.account_id, Collection::Mailbox);
|
|
if subscribe {
|
|
if !changes.subscribers.contains(&account_id) {
|
|
changes.subscribers.push(account_id);
|
|
}
|
|
} else {
|
|
changes.subscribers.retain(|id| *id != account_id);
|
|
}
|
|
}
|
|
(
|
|
Key::Property(MailboxProperty::Role),
|
|
Value::Element(MailboxValue::Role(role)),
|
|
) => {
|
|
changes.role = role;
|
|
}
|
|
(Key::Property(MailboxProperty::Role), Value::Null) => {
|
|
changes.role = SpecialUse::None;
|
|
}
|
|
(Key::Property(MailboxProperty::SortOrder), Value::Number(value)) => {
|
|
changes.sort_order = Some(value.cast_to_u64() as u32);
|
|
}
|
|
(Key::Property(MailboxProperty::ShareWith), value) => {
|
|
match JmapRights::acl_set::<mailbox::Mailbox>(value) {
|
|
Ok(acls) => {
|
|
has_acl_changes = true;
|
|
changes.acls = acls;
|
|
continue;
|
|
}
|
|
Err(err) => {
|
|
return Ok(Err(err));
|
|
}
|
|
}
|
|
}
|
|
(Key::Property(MailboxProperty::Pointer(pointer)), value)
|
|
if matches!(
|
|
pointer.first(),
|
|
Some(JsonPointerItem::Key(Key::Property(
|
|
MailboxProperty::ShareWith
|
|
)))
|
|
) =>
|
|
{
|
|
let mut pointer = pointer.iter();
|
|
pointer.next();
|
|
|
|
match JmapRights::acl_patch::<mailbox::Mailbox>(changes.acls, pointer, value) {
|
|
Ok(acls) => {
|
|
has_acl_changes = true;
|
|
changes.acls = acls;
|
|
continue;
|
|
}
|
|
Err(err) => {
|
|
return Ok(Err(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
(Key::Property(MailboxProperty::Id), value) => {
|
|
if update
|
|
.as_ref()
|
|
.map(|(document_id, _)| Id::from(*document_id))
|
|
.is_none_or(|expected| !crate::matches_id(&value, expected))
|
|
{
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::Id)
|
|
.with_description("The id property is immutable.".to_string())));
|
|
}
|
|
}
|
|
_ => {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(property.into_owned())
|
|
.with_description("Invalid property or value.".to_string())));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate depth and circular parent-child relationship
|
|
if update
|
|
.as_ref()
|
|
.is_none_or(|(_, m)| m.inner.parent_id != changes.parent_id)
|
|
{
|
|
let mut mailbox_parent_id = changes.parent_id;
|
|
let current_mailbox_id = update
|
|
.as_ref()
|
|
.map_or(u32::MAX, |(mailbox_id, _)| *mailbox_id + 1);
|
|
let mut success = false;
|
|
for depth in 0..self.core.email.mailbox_max_depth {
|
|
if mailbox_parent_id == current_mailbox_id {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::ParentId)
|
|
.with_description("Mailbox cannot be a parent of itself.")));
|
|
} else if mailbox_parent_id == 0 {
|
|
if depth == 0 && ctx.is_shared {
|
|
return Ok(Err(SetError::forbidden()
|
|
.with_description("You are not allowed to create root folders.")));
|
|
}
|
|
success = true;
|
|
break;
|
|
}
|
|
let parent_document_id = mailbox_parent_id - 1;
|
|
|
|
if let Some(mailbox_) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
ctx.account_id,
|
|
Collection::Mailbox,
|
|
parent_document_id,
|
|
))
|
|
.await?
|
|
{
|
|
let mailbox = mailbox_
|
|
.unarchive::<email::mailbox::Mailbox>()
|
|
.caused_by(trc::location!())?;
|
|
if depth == 0
|
|
&& ctx.is_shared
|
|
&& !mailbox
|
|
.acls
|
|
.effective_acl(ctx.access_token)
|
|
.contains(Acl::CreateChild)
|
|
{
|
|
return Ok(Err(SetError::forbidden().with_description(
|
|
"You are not allowed to create sub mailboxes under this mailbox.",
|
|
)));
|
|
}
|
|
|
|
mailbox_parent_id = mailbox.parent_id.into();
|
|
} else if ctx.mailbox_ids.contains(parent_document_id) {
|
|
// Parent mailbox is probably created within the same request
|
|
success = true;
|
|
break;
|
|
} else {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::ParentId)
|
|
.with_description("Mailbox parent does not exist.")));
|
|
}
|
|
}
|
|
|
|
if !success {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::ParentId)
|
|
.with_description(
|
|
"Mailbox parent-child relationship is too deep.",
|
|
)));
|
|
}
|
|
}
|
|
|
|
let cached_mailboxes = self.get_cached_messages(ctx.account_id).await?;
|
|
|
|
// Verify that the mailbox role is unique.
|
|
if update
|
|
.as_ref()
|
|
.is_none_or(|(_, m)| m.inner.role != changes.role)
|
|
{
|
|
if !matches!(changes.role, SpecialUse::None)
|
|
&& cached_mailboxes.mailbox_by_role(&changes.role).is_some()
|
|
{
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::Role)
|
|
.with_description(format!(
|
|
"A mailbox with role '{}' already exists.",
|
|
changes.role.as_str().unwrap_or_default()
|
|
))));
|
|
}
|
|
|
|
// Role of internal folders cannot be modified
|
|
if update.as_ref().is_some_and(|(document_id, _)| {
|
|
*document_id == INBOX_ID || *document_id == TRASH_ID || *document_id == JUNK_ID
|
|
}) {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::Role)
|
|
.with_description(
|
|
"You are not allowed to change the role of Inbox, Junk or Trash folders.",
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Verify that the mailbox name is unique.
|
|
if !changes.name.is_empty() {
|
|
// Obtain parent mailbox id
|
|
let lower_name = changes.name.to_lowercase();
|
|
if update
|
|
.as_ref()
|
|
.is_none_or(|(_, m)| m.inner.name != changes.name)
|
|
&& let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| {
|
|
m.name.to_lowercase() == lower_name
|
|
&& m.parent_id().map_or(0, |id| id + 1) == changes.parent_id
|
|
})
|
|
{
|
|
return Ok(Err(SetError::already_exists()
|
|
.with_existing_id(Id::from(existing.document_id))
|
|
.with_description(format!(
|
|
"A mailbox with name '{}' already exists.",
|
|
changes.name
|
|
))));
|
|
}
|
|
} else {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(MailboxProperty::Name)
|
|
.with_description("Mailbox name cannot be empty.")));
|
|
}
|
|
|
|
// Refresh ACLs
|
|
let current = update.map(|(_, current)| current);
|
|
if has_acl_changes {
|
|
// inbuxa: MA-D0: a group's members don't share its mailboxes on.
|
|
if ctx.access_token.is_group_member_only(ctx.account_id) {
|
|
return Ok(Err(SetError::forbidden()
|
|
.with_property(MailboxProperty::ShareWith)
|
|
.with_description(
|
|
"This mailbox belongs to a group. Only an administrator can change who has it.",
|
|
)));
|
|
}
|
|
|
|
// inbuxa: MA-C: with mail sharing off, nobody here starts or
|
|
// widens a share (narrowing or ending one is always allowed)
|
|
let before = current.as_ref().map(|m| m.inner.acls.as_slice()).unwrap_or_default();
|
|
let widens = changes.acls.iter().any(|grant| {
|
|
let had = before
|
|
.iter()
|
|
.find(|old| old.account_id == grant.account_id)
|
|
.map_or(0, |old| old.grants.clone().into_inner());
|
|
grant.grants.clone().into_inner() & !had != 0
|
|
});
|
|
if widens
|
|
&& !ctx.access_token.has_permission(Permission::Impersonate)
|
|
&& !self.mail_sharing_allowed(ctx.account_id).await?
|
|
{
|
|
return Ok(Err(SetError::forbidden()
|
|
.with_property(MailboxProperty::ShareWith)
|
|
.with_description(
|
|
"Your organization has turned off sharing mail folders. A shared mailbox or a group can be set up by an administrator instead.",
|
|
)));
|
|
}
|
|
|
|
if !changes.acls.is_empty()
|
|
&& let Err(err) = self.acl_validate(ctx.account_id, &changes.acls).await
|
|
{
|
|
return Ok(Err(err.into()));
|
|
}
|
|
|
|
self.refresh_acls(
|
|
&changes.acls,
|
|
current.as_ref().map(|m| m.inner.acls.as_slice()),
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
// Validate
|
|
Ok(Ok(ObjectIndexBuilder::new()
|
|
.with_changes(changes)
|
|
.with_current_opt(current)))
|
|
}
|
|
}
|