Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use email::cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess};
use jmap_proto::{
method::get::{GetRequest, GetResponse},
object::mailbox::{Mailbox, MailboxProperty, MailboxValue},
};
use jmap_tools::{Map, Value};
use std::future::Future;
use store::ahash::AHashSet;
use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse};
use crate::{api::acl::JmapRights, changes::state::JmapCacheState};
pub trait MailboxGet: Sync + Send {
fn mailbox_get(
&self,
request: GetRequest<Mailbox>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse<Mailbox>>> + Send;
}
impl MailboxGet for Server {
async fn mailbox_get(
&self,
mut request: GetRequest<Mailbox>,
access_token: &AccessToken,
) -> trc::Result<GetResponse<Mailbox>> {
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[
MailboxProperty::Id,
MailboxProperty::Name,
MailboxProperty::ParentId,
MailboxProperty::Role,
MailboxProperty::SortOrder,
MailboxProperty::IsSubscribed,
MailboxProperty::TotalEmails,
MailboxProperty::UnreadEmails,
MailboxProperty::TotalThreads,
MailboxProperty::UnreadThreads,
MailboxProperty::MyRights,
]);
let account_id = request.account_id.document_id();
let personal_id = access_token.personal_id(account_id, Collection::Mailbox);
let cache = self.get_cached_messages(account_id).await?;
let shared_ids = if access_token.is_shared(account_id) {
cache.shared_mailboxes(access_token, Acl::Read).into()
} else {
None
};
let ids = if let Some(ids) = ids {
ids
} else {
cache
.mailboxes
.index
.keys()
.filter(|id| shared_ids.as_ref().is_none_or(|ids| ids.contains(**id)))
.copied()
.take(self.core.jmap.get_max_objects)
.map(Into::into)
.collect::<Vec<_>>()
};
let mut response = GetResponse {
account_id: request.account_id.into(),
state: Some(cache.get_state(true)),
list: Vec::with_capacity(ids.len()),
not_found: not_found_ids,
};
for id in ids {
// Obtain the mailbox object
let document_id = id.document_id();
let cached_mailbox = if let Some(mailbox) =
cache.mailbox_by_id(&document_id).filter(|_| {
shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(document_id))
}) {
mailbox
} else {
response.push_not_found(id);
continue;
};
let mut mailbox = Map::with_capacity(properties.len());
for property in &properties {
let value = match property {
MailboxProperty::Id => Value::Element(MailboxValue::Id(id)),
MailboxProperty::Name => Value::Str(cached_mailbox.name.to_string().into()),
MailboxProperty::Role => match cached_mailbox.role {
SpecialUse::None => Value::Null,
role => Value::Element(MailboxValue::Role(role)),
},
MailboxProperty::SortOrder => {
Value::Number(cached_mailbox.sort_order().unwrap_or_default().into())
}
MailboxProperty::ParentId => {
if let Some(parent_id) = cached_mailbox.parent_id() {
Value::Element(MailboxValue::Id(parent_id.into()))
} else {
Value::Null
}
}
MailboxProperty::TotalEmails => {
Value::Number(cache.in_mailbox(document_id).count().into())
}
MailboxProperty::UnreadEmails => Value::Number(
cache
.in_mailbox_without_keyword(document_id, &Keyword::Seen)
.count()
.into(),
),
MailboxProperty::TotalThreads => Value::Number(
cache
.in_mailbox(document_id)
.map(|m| m.thread_id)
.collect::<AHashSet<_>>()
.len()
.into(),
),
MailboxProperty::UnreadThreads => Value::Number(
cache
.in_mailbox_without_keyword(document_id, &Keyword::Seen)
.map(|m| m.thread_id)
.collect::<AHashSet<_>>()
.len()
.into(),
),
MailboxProperty::MyRights => {
if access_token.is_shared(account_id) {
JmapRights::rights::<Mailbox>(
cached_mailbox.acls.as_slice().effective_acl(access_token),
)
} else {
JmapRights::all_rights::<Mailbox>()
}
}
MailboxProperty::IsSubscribed => {
Value::Bool(cached_mailbox.subscribers.contains(&personal_id))
}
MailboxProperty::ShareWith => JmapRights::share_with::<Mailbox>(
account_id,
access_token,
&cached_mailbox.acls,
),
_ => Value::Null,
};
mailbox.insert_unchecked(property.clone(), value);
}
// Add result to response
response.list.push(mailbox.into());
}
Ok(response)
}
}
+9
View File
@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod get;
pub mod query;
pub mod set;
+283
View File
@@ -0,0 +1,283 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use common::{Server, auth::AccessToken};
use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess};
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
object::mailbox::{Mailbox, MailboxComparator, MailboxFilter},
};
use std::{collections::BTreeMap, future::Future};
use store::{
ahash::AHashMap,
roaring::RoaringBitmap,
search::{SearchComparator, SearchFilter, SearchQuery},
write::SearchIndex,
};
use types::{acl::Acl, collection::Collection, special_use::SpecialUse};
pub trait MailboxQuery: Sync + Send {
fn mailbox_query(
&self,
request: QueryRequest<Mailbox>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
impl MailboxQuery for Server {
async fn mailbox_query(
&self,
mut request: QueryRequest<Mailbox>,
access_token: &AccessToken,
) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id();
let personal_id = access_token.personal_id(account_id, Collection::Mailbox);
let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false);
let filter_as_tree = request.arguments.filter_as_tree.unwrap_or(false);
let mut filters = Vec::with_capacity(request.filter.len());
let mailboxes = self.get_cached_messages(account_id).await?;
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => {
match cond {
MailboxFilter::ParentId(parent_id) => {
let parent_id = parent_id
.and_then(|id| id.try_unwrap().map(|id| id.document_id()))
.unwrap_or(u32::MAX);
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| mailbox.parent_id == parent_id)
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
}
MailboxFilter::Name(name) => {
#[cfg(any(feature = "dev_mode", feature = "test_mode"))]
{
// Used for concurrent requests tests
if name == "__sleep" {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
let name = name.to_lowercase();
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| mailbox.name.to_lowercase().contains(&name))
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
}
MailboxFilter::Role(role) => {
if let Some(role) = role {
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| mailbox.role == role)
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
} else {
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| matches!(mailbox.role, SpecialUse::None))
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
}
}
MailboxFilter::HasAnyRole(has_role) => {
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| {
matches!(mailbox.role, SpecialUse::None) != has_role
})
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
}
MailboxFilter::IsSubscribed(is_subscribed) => {
filters.push(SearchFilter::is_in_set(
mailboxes
.mailboxes
.items
.iter()
.filter(|mailbox| {
mailbox.subscribers.contains(&personal_id) == is_subscribed
})
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),
));
}
MailboxFilter::_T(other) => {
return Err(trc::JmapEvent::UnsupportedFilter
.into_err()
.details(other));
}
}
}
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
// Sort as tree
if sort_as_tree {
let sorted_set = mailboxes
.mailboxes
.items
.iter()
.map(|mailbox| (mailbox.path.as_str(), mailbox.document_id))
.collect::<BTreeMap<_, _>>();
comparators.push(SearchComparator::sorted_set(
sorted_set
.into_iter()
.enumerate()
.map(|(i, (_, v))| (v, i as u32))
.collect(),
true,
));
}
// Parse sort criteria
for comparator in request
.sort
.take()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::ascending(MailboxComparator::ParentId)])
{
comparators.push(match comparator.property {
MailboxComparator::Name => {
let sorted_set = mailboxes
.mailboxes
.items
.iter()
.map(|mailbox| (mailbox.name.as_str(), mailbox.document_id))
.collect::<BTreeMap<_, _>>();
SearchComparator::sorted_set(
sorted_set
.into_iter()
.enumerate()
.map(|(i, (_, v))| (v, i as u32))
.collect(),
comparator.is_ascending,
)
}
MailboxComparator::SortOrder => {
let sorted_set = mailboxes
.mailboxes
.items
.iter()
.map(|mailbox| (mailbox.document_id, mailbox.sort_order))
.collect::<AHashMap<_, _>>();
SearchComparator::sorted_set(sorted_set, comparator.is_ascending)
}
MailboxComparator::ParentId => {
let sorted_set = mailboxes
.mailboxes
.items
.iter()
.map(|mailbox| {
(
mailbox.document_id,
mailbox.parent_id().map(|id| id + 1).unwrap_or_default(),
)
})
.collect::<AHashMap<_, _>>();
SearchComparator::sorted_set(sorted_set, comparator.is_ascending)
}
MailboxComparator::_T(other) => {
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
}
});
}
let mut results = SearchQuery::new(SearchIndex::InMemory)
.with_filters(filters)
.with_comparators(comparators)
.with_mask(if access_token.is_shared(account_id) {
mailboxes.shared_mailboxes(access_token, Acl::Read)
} else {
mailboxes
.mailboxes
.items
.iter()
.map(|m| m.document_id)
.collect()
})
.filter();
// Filter as tree
if filter_as_tree {
let mut new_results = RoaringBitmap::new();
for document_id in results.results() {
let mut check_id = document_id;
for _ in 0..self.core.email.mailbox_max_depth {
if let Some(mailbox) = mailboxes.mailbox_by_id(&check_id) {
if let Some(parent_id) = mailbox.parent_id() {
if results.results().contains(parent_id) {
check_id = parent_id;
} else {
break;
}
} else {
new_results.insert(document_id);
}
}
}
}
results.update_results(new_results);
}
let mut response = QueryResponseBuilder::new(
results.results().len() as usize,
self.core.jmap.query_max_results,
mailboxes.get_state(true),
&request,
);
for document_id in results.into_sorted() {
if !response.add(0, document_id) {
break;
}
}
response.build()
}
}
+633
View File
@@ -0,0 +1,633 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
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::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 {
if !changes.acls.is_empty()
&& let Err(err) = self.acl_validate(&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)))
}
}