The AGPL asks a modified version to carry prominent notices saying it was modified, and giving a date. Publishing the source is the conveyance that asks for it, so it wants doing before the repository is public rather than at the release. Every upstream file the fork changed now says so in its header, beneath the notice it came with: 164 files, found by diffing against the upstream snapshot branch rather than by guessing, so the list is what actually differs. Files the fork wrote itself already carry their own copyright and need nothing. Upstream's notices are untouched, which its licence requires and which was already true. The README says the same thing in prose, since the obligation is on the work as a whole and not only its Rust files. Builds unchanged: the server and the test binary both compile.
524 lines
19 KiB
Rust
524 lines
19 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::{
|
|
core::{MailboxId, Session, SessionData, State},
|
|
op::ImapContext,
|
|
spawn_op,
|
|
};
|
|
use common::{
|
|
auth::AccessToken, ipc::CacheInvalidation, network::SessionStream, sharing::EffectiveAcl,
|
|
storage::index::ObjectIndexBuilder,
|
|
};
|
|
use compact_str::ToCompactString;
|
|
use imap_proto::{
|
|
Command, ResponseCode, StatusResponse,
|
|
protocol::acl::{
|
|
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
|
|
},
|
|
receiver::Request,
|
|
};
|
|
use registry::schema::enums::Permission;
|
|
use std::time::Instant;
|
|
use store::{
|
|
ValueKey,
|
|
write::{AlignedBytes, Archive, BatchBuilder},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::{Acl, AclGrant},
|
|
collection::Collection,
|
|
};
|
|
use utils::map::bitmap::Bitmap;
|
|
|
|
impl<T: SessionStream> Session<T> {
|
|
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
|
// Validate access
|
|
self.assert_has_permission(Permission::ImapAclGet)?;
|
|
|
|
let op_start = Instant::now();
|
|
let arguments = request.parse_acl(self.is_utf8)?;
|
|
let is_utf8 = self.version.is_rev2() || self.is_utf8;
|
|
let data = self.state.session_data();
|
|
|
|
spawn_op!(data, {
|
|
let (mailbox_id, mailbox_, _) = data
|
|
.get_acl_mailbox(&arguments, true)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
let mut permissions = Vec::new();
|
|
let mailbox = mailbox_
|
|
.to_unarchived::<email::mailbox::Mailbox>()
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
|
|
// Add the current user if they are the owner or a group member
|
|
if data.access_token.is_member(mailbox_id.account_id) {
|
|
let account_name = data
|
|
.server
|
|
.account(mailbox_id.account_id)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?
|
|
.name()
|
|
.to_string();
|
|
|
|
permissions.push((
|
|
account_name,
|
|
vec![
|
|
Rights::Read,
|
|
Rights::Lookup,
|
|
Rights::Insert,
|
|
Rights::DeleteMessages,
|
|
Rights::Expunge,
|
|
Rights::Seen,
|
|
Rights::Write,
|
|
Rights::CreateMailbox,
|
|
Rights::DeleteMailbox,
|
|
Rights::Post,
|
|
Rights::Administer,
|
|
],
|
|
));
|
|
}
|
|
|
|
for item in mailbox.inner.acls.iter() {
|
|
if item.account_id == mailbox_id.account_id {
|
|
// Skip the current user, as they are already added above
|
|
continue;
|
|
}
|
|
|
|
let mut rights = Vec::new();
|
|
|
|
for acl in Bitmap::from(&item.grants) {
|
|
match acl {
|
|
Acl::Read => {
|
|
rights.push(Rights::Lookup);
|
|
}
|
|
Acl::Modify => {
|
|
rights.push(Rights::CreateMailbox);
|
|
}
|
|
Acl::Delete => {
|
|
rights.push(Rights::DeleteMailbox);
|
|
}
|
|
Acl::ReadItems => {
|
|
rights.push(Rights::Read);
|
|
}
|
|
Acl::AddItems => {
|
|
rights.push(Rights::Insert);
|
|
}
|
|
Acl::ModifyItems => {
|
|
rights.push(Rights::Write);
|
|
rights.push(Rights::Seen);
|
|
}
|
|
Acl::RemoveItems => {
|
|
rights.push(Rights::DeleteMessages);
|
|
rights.push(Rights::Expunge);
|
|
}
|
|
Acl::CreateChild => {
|
|
rights.push(Rights::CreateMailbox);
|
|
}
|
|
Acl::Share => {
|
|
rights.push(Rights::Administer);
|
|
}
|
|
Acl::Submit => {
|
|
rights.push(Rights::Post);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
let account_name = data
|
|
.server
|
|
.account(item.account_id.into())
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?
|
|
.name()
|
|
.to_string();
|
|
|
|
permissions.push((account_name, rights));
|
|
}
|
|
|
|
trc::event!(
|
|
Imap(trc::ImapEvent::GetAcl),
|
|
SpanId = data.session_id,
|
|
MailboxName = arguments.mailbox_name.clone(),
|
|
AccountId = mailbox_id.account_id,
|
|
MailboxId = mailbox_id.mailbox_id,
|
|
Total = permissions.len(),
|
|
Elapsed = op_start.elapsed()
|
|
);
|
|
|
|
data.write_bytes(
|
|
StatusResponse::completed(Command::GetAcl)
|
|
.with_tag(arguments.tag)
|
|
.serialize(
|
|
GetAclResponse {
|
|
mailbox_name: arguments.mailbox_name.to_string(),
|
|
permissions,
|
|
}
|
|
.into_bytes(is_utf8),
|
|
),
|
|
)
|
|
.await
|
|
})
|
|
}
|
|
|
|
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
|
// Validate access
|
|
self.assert_has_permission(Permission::ImapMyRights)?;
|
|
|
|
let op_start = Instant::now();
|
|
let arguments = request.parse_acl(self.is_utf8)?;
|
|
let data = self.state.session_data();
|
|
let is_utf8 = self.version.is_rev2() || self.is_utf8;
|
|
|
|
spawn_op!(data, {
|
|
let (mailbox_id, mailbox_, access_token) = data
|
|
.get_acl_mailbox(&arguments, false)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
let mailbox = mailbox_
|
|
.to_unarchived::<email::mailbox::Mailbox>()
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
let rights = if access_token.is_shared(mailbox_id.account_id) {
|
|
let acl = mailbox.inner.acls.effective_acl(&access_token);
|
|
let mut rights = Vec::with_capacity(5);
|
|
if acl.contains(Acl::ReadItems) {
|
|
rights.push(Rights::Read);
|
|
rights.push(Rights::Lookup);
|
|
}
|
|
if acl.contains(Acl::AddItems) {
|
|
rights.push(Rights::Insert);
|
|
}
|
|
if acl.contains(Acl::RemoveItems) {
|
|
rights.push(Rights::DeleteMessages);
|
|
rights.push(Rights::Expunge);
|
|
}
|
|
if acl.contains(Acl::ModifyItems) {
|
|
rights.push(Rights::Seen);
|
|
rights.push(Rights::Write);
|
|
}
|
|
if acl.contains(Acl::CreateChild) {
|
|
rights.push(Rights::CreateMailbox);
|
|
}
|
|
if acl.contains(Acl::Delete) {
|
|
rights.push(Rights::DeleteMailbox);
|
|
}
|
|
if acl.contains(Acl::Submit) {
|
|
rights.push(Rights::Post);
|
|
}
|
|
rights
|
|
} else {
|
|
vec![
|
|
Rights::Read,
|
|
Rights::Lookup,
|
|
Rights::Insert,
|
|
Rights::DeleteMessages,
|
|
Rights::Expunge,
|
|
Rights::Seen,
|
|
Rights::Write,
|
|
Rights::CreateMailbox,
|
|
Rights::DeleteMailbox,
|
|
Rights::Post,
|
|
Rights::Administer,
|
|
]
|
|
};
|
|
|
|
trc::event!(
|
|
Imap(trc::ImapEvent::MyRights),
|
|
SpanId = data.session_id,
|
|
MailboxName = arguments.mailbox_name.clone(),
|
|
AccountId = mailbox_id.account_id,
|
|
MailboxId = mailbox_id.mailbox_id,
|
|
Details = rights
|
|
.iter()
|
|
.map(|r| trc::Value::String(r.to_compact_string()))
|
|
.collect::<Vec<_>>(),
|
|
Elapsed = op_start.elapsed()
|
|
);
|
|
|
|
data.write_bytes(
|
|
StatusResponse::completed(Command::MyRights)
|
|
.with_tag(arguments.tag)
|
|
.serialize(
|
|
MyRightsResponse {
|
|
mailbox_name: arguments.mailbox_name.to_string(),
|
|
rights,
|
|
}
|
|
.into_bytes(is_utf8),
|
|
),
|
|
)
|
|
.await
|
|
})
|
|
}
|
|
|
|
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
|
// Validate access
|
|
self.assert_has_permission(Permission::ImapAclSet)?;
|
|
|
|
let op_start = Instant::now();
|
|
let command = request.command;
|
|
let arguments = request.parse_acl(self.is_utf8)?;
|
|
let data = self.state.session_data();
|
|
|
|
spawn_op!(data, {
|
|
// Validate mailbox
|
|
let (mailbox_id, current_mailbox, _) = data
|
|
.get_acl_mailbox(&arguments, true)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
let current_mailbox = current_mailbox
|
|
.into_deserialized::<email::mailbox::Mailbox>()
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
|
|
// Obtain principal id
|
|
let acl_account_id = data
|
|
.server
|
|
.account_id_from_email(arguments.identifier.as_ref().unwrap(), false)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?
|
|
.ok_or_else(|| {
|
|
trc::ImapEvent::Error
|
|
.into_err()
|
|
.details("Account does not exist")
|
|
.id(arguments.tag.to_string())
|
|
.caused_by(trc::location!())
|
|
})?;
|
|
|
|
// inbuxa: MT-3: grants stay within the owner's tenant, refused
|
|
// as if the account didn't exist
|
|
let owner_tenant = data
|
|
.server
|
|
.try_account(mailbox_id.account_id)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?
|
|
.and_then(|owner| owner.id_tenant);
|
|
if data
|
|
.server
|
|
.try_account(acl_account_id)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?
|
|
.is_none_or(|grantee| grantee.id_tenant != owner_tenant)
|
|
{
|
|
return Err(trc::ImapEvent::Error
|
|
.into_err()
|
|
.details("Account does not exist")
|
|
.id(arguments.tag.to_string())
|
|
.caused_by(trc::location!()));
|
|
}
|
|
|
|
// Prepare changes
|
|
let mut mailbox = current_mailbox.inner.clone();
|
|
let (op, rights) = arguments
|
|
.mod_rights
|
|
.map(|mr| {
|
|
(
|
|
mr.op,
|
|
Bitmap::from_iter(mr.rights.into_iter().map(Acl::from)),
|
|
)
|
|
})
|
|
.unwrap_or_else(|| (ModRightsOp::Replace, Bitmap::new()));
|
|
|
|
if let Some(item) = mailbox
|
|
.acls
|
|
.iter_mut()
|
|
.find(|item| item.account_id == acl_account_id)
|
|
{
|
|
match op {
|
|
ModRightsOp::Replace => {
|
|
if !rights.is_empty() {
|
|
item.grants = rights;
|
|
} else {
|
|
mailbox
|
|
.acls
|
|
.retain(|item| item.account_id != acl_account_id);
|
|
}
|
|
}
|
|
ModRightsOp::Add => {
|
|
item.grants.union(&rights);
|
|
}
|
|
ModRightsOp::Remove => {
|
|
for right in rights {
|
|
item.grants.remove(right);
|
|
}
|
|
if item.grants.is_empty() {
|
|
mailbox
|
|
.acls
|
|
.retain(|item| item.account_id != acl_account_id);
|
|
}
|
|
}
|
|
}
|
|
} else if !rights.is_empty() {
|
|
match op {
|
|
ModRightsOp::Add | ModRightsOp::Replace => {
|
|
mailbox.acls.push(AclGrant {
|
|
account_id: acl_account_id,
|
|
grants: rights,
|
|
});
|
|
}
|
|
ModRightsOp::Remove => (),
|
|
}
|
|
}
|
|
|
|
if mailbox.acls.len() > data.server.core.groupware.max_shares_per_item {
|
|
return Err(trc::ImapEvent::Error
|
|
.into_err()
|
|
.details("Maximum shares per item exceeded")
|
|
.caused_by(trc::location!()));
|
|
}
|
|
|
|
let grants = mailbox
|
|
.acls
|
|
.iter()
|
|
.map(|r| trc::Value::from(r.account_id))
|
|
.collect::<Vec<_>>();
|
|
|
|
// Write changes
|
|
let mut batch = BatchBuilder::new();
|
|
batch
|
|
.with_account_id(mailbox_id.account_id)
|
|
.with_collection(Collection::Mailbox)
|
|
.with_document(mailbox_id.mailbox_id)
|
|
.custom(
|
|
ObjectIndexBuilder::new()
|
|
.with_changes(mailbox)
|
|
.with_current(current_mailbox),
|
|
)
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
|
|
if !batch.is_empty() {
|
|
data.server
|
|
.commit_batch(batch)
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
}
|
|
|
|
// Invalidate ACLs
|
|
data.server
|
|
.invalidate_caches(CacheInvalidation::AccessToken(acl_account_id).into())
|
|
.await
|
|
.imap_ctx(&arguments.tag, trc::location!())?;
|
|
|
|
trc::event!(
|
|
Imap(trc::ImapEvent::SetAcl),
|
|
SpanId = data.session_id,
|
|
MailboxName = arguments.mailbox_name.clone(),
|
|
AccountId = mailbox_id.account_id,
|
|
MailboxId = mailbox_id.mailbox_id,
|
|
Details = grants,
|
|
Elapsed = op_start.elapsed()
|
|
);
|
|
|
|
data.write_bytes(
|
|
StatusResponse::completed(command)
|
|
.with_tag(arguments.tag)
|
|
.into_bytes(),
|
|
)
|
|
.await
|
|
})
|
|
}
|
|
|
|
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
|
// Validate access
|
|
self.assert_has_permission(Permission::ImapListRights)?;
|
|
|
|
let op_start = Instant::now();
|
|
let arguments = request.parse_acl(self.is_utf8)?;
|
|
|
|
trc::event!(
|
|
Imap(trc::ImapEvent::ListRights),
|
|
SpanId = self.session_id,
|
|
MailboxName = arguments.mailbox_name.clone(),
|
|
Elapsed = op_start.elapsed()
|
|
);
|
|
|
|
self.write_bytes(
|
|
StatusResponse::completed(Command::ListRights)
|
|
.with_tag(arguments.tag)
|
|
.serialize(
|
|
ListRightsResponse {
|
|
mailbox_name: arguments.mailbox_name,
|
|
identifier: arguments.identifier.unwrap(),
|
|
permissions: vec![
|
|
vec![Rights::Read],
|
|
vec![Rights::Lookup],
|
|
vec![Rights::Write, Rights::Seen],
|
|
vec![Rights::Insert],
|
|
vec![Rights::Expunge, Rights::DeleteMessages],
|
|
vec![Rights::CreateMailbox],
|
|
vec![Rights::DeleteMailbox],
|
|
vec![Rights::Post],
|
|
vec![Rights::Administer],
|
|
],
|
|
}
|
|
.into_bytes(self.version.is_rev2() || self.is_utf8),
|
|
),
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<bool> {
|
|
match &self.state {
|
|
State::Authenticated { data } | State::Selected { data, .. } => data
|
|
.access_token
|
|
.enforce_permission(permission)
|
|
.map(|_| true),
|
|
State::NotAuthenticated { .. } => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: SessionStream> SessionData<T> {
|
|
async fn get_acl_mailbox(
|
|
&self,
|
|
arguments: &Arguments,
|
|
validate: bool,
|
|
) -> trc::Result<(MailboxId, Archive<AlignedBytes>, AccessToken)> {
|
|
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
|
|
if let Some(values) = self
|
|
.server
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
mailbox.account_id,
|
|
Collection::Mailbox,
|
|
mailbox.mailbox_id,
|
|
))
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
let access_token = self
|
|
.refresh_access_token()
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
if !validate
|
|
|| access_token.is_member(mailbox.account_id)
|
|
|| values
|
|
.unarchive::<email::mailbox::Mailbox>()
|
|
.caused_by(trc::location!())?
|
|
.acls
|
|
.effective_acl(&access_token)
|
|
.contains(Acl::Share)
|
|
{
|
|
Ok((mailbox, values, access_token))
|
|
} else {
|
|
Err(trc::ImapEvent::Error
|
|
.into_err()
|
|
.details("You do not have enough permissions to perform this operation.")
|
|
.code(ResponseCode::NoPerm))
|
|
}
|
|
} else {
|
|
Err(trc::ImapEvent::Error
|
|
.caused_by(trc::location!())
|
|
.details("Mailbox does not exist."))
|
|
}
|
|
} else {
|
|
Err(trc::ImapEvent::Error
|
|
.into_err()
|
|
.details("Mailbox does not exist."))
|
|
}
|
|
}
|
|
}
|