Files
Stalwart/crates/jmap/src/mailbox/set.rs
2024-02-13 15:00:58 +01:00

977 lines
39 KiB
Rust

/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use jmap_proto::{
error::{
method::MethodError,
set::{SetError, SetErrorType},
},
method::set::{SetRequest, SetResponse},
object::{
index::{IndexAs, IndexProperty, ObjectIndexBuilder},
mailbox::SetArguments,
Object,
},
response::references::EvalObjectReferences,
types::{
acl::Acl,
collection::Collection,
id::Id,
property::Property,
state::StateChange,
type_state::DataType,
value::{MaybePatchValue, SetValue, Value},
},
};
use store::{
query::Filter,
roaring::RoaringBitmap,
write::{
assert::{AssertValue, HashedValue},
log::ChangeLogBuilder,
BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE,
},
};
use crate::{
auth::{acl::EffectiveAcl, AccessToken},
JMAP,
};
#[allow(unused_imports)]
use super::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID};
struct SetContext<'x> {
account_id: u32,
access_token: &'x AccessToken,
is_shared: bool,
response: SetResponse,
mailbox_ids: RoaringBitmap,
will_destroy: Vec<Id>,
}
pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::Name)
.index_as(IndexAs::Text {
tokenize: true,
index: true,
})
.required(),
IndexProperty::new(Property::Role).index_as(IndexAs::Text {
tokenize: false,
index: true,
}),
IndexProperty::new(Property::Role).index_as(IndexAs::HasProperty),
IndexProperty::new(Property::ParentId).index_as(IndexAs::Integer),
IndexProperty::new(Property::SortOrder).index_as(IndexAs::Integer),
IndexProperty::new(Property::IsSubscribed).index_as(IndexAs::IntegerList),
IndexProperty::new(Property::Acl).index_as(IndexAs::Acl),
];
impl JMAP {
#[allow(clippy::blocks_in_conditions)]
pub async fn mailbox_set(
&self,
mut request: SetRequest<SetArguments>,
access_token: &AccessToken,
) -> Result<SetResponse, MethodError> {
// 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 mut ctx = SetContext {
account_id,
is_shared: access_token.is_shared(account_id),
access_token,
response: self
.prepare_set_response(&request, Collection::Mailbox)
.await?,
mailbox_ids: self.mailbox_get_or_create(account_id).await?,
will_destroy: request.unwrap_destroy(),
};
// Process creates
let mut changes = ChangeLogBuilder::new();
'create: for (id, object) in request.unwrap_create() {
match self.mailbox_set_item(object, None, &ctx).await? {
Ok(builder) => {
let mut batch = BatchBuilder::new();
let document_id = self
.assign_document_id(account_id, Collection::Mailbox)
.await?;
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
if let Value::Id(parent_id) =
builder.changes().unwrap().get(&Property::ParentId)
{
let parent_id = parent_id.document_id();
if parent_id > 0 {
batch
.update_document(parent_id - 1)
.assert_value(Property::Value, AssertValue::Some);
}
}
batch.create_document(document_id).custom(builder);
changes.log_insert(Collection::Mailbox, document_id);
ctx.mailbox_ids.insert(document_id);
match self.store.write(batch.build()).await {
Ok(_) => {
ctx.response.created(id, document_id);
}
Err(store::Error::AssertValueFailed) => {
ctx.response.not_created.append(
id,
SetError::forbidden().with_description(
"Another process deleted the parent mailbox, please try again.",
),
);
continue 'create;
}
Err(err) => {
tracing::error!(
event = "error",
context = "mailbox_set",
account_id = account_id,
error = ?err,
"Failed to update mailbox(es).");
return Err(MethodError::ServerPartialFail);
}
}
}
Err(err) => {
ctx.response.not_created.append(id, err);
continue 'create;
}
}
}
// Process updates
'update: for (id, object) in request.unwrap_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;
}
// Obtain mailbox
let document_id = id.document_id();
if let Some(mailbox) = self
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::Mailbox,
document_id,
Property::Value,
)
.await?
{
// Validate ACL
if ctx.is_shared {
let acl = mailbox.inner.effective_acl(access_token);
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.properties.contains_key(&Property::Acl)
&& !acl.contains(Acl::Administer)
{
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) => {
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
if let Value::Id(parent_id) =
builder.changes().unwrap().get(&Property::ParentId)
{
let parent_id = parent_id.document_id();
if parent_id > 0 {
batch
.update_document(parent_id - 1)
.assert_value(Property::Value, AssertValue::Some);
}
}
batch.update_document(document_id).custom(builder);
if !batch.is_empty() {
match self.store.write(batch.build()).await {
Ok(_) => {
changes.log_update(Collection::Mailbox, document_id);
}
Err(store::Error::AssertValueFailed) => {
ctx.response.not_updated.append(id, SetError::forbidden().with_description(
"Another process modified this mailbox, please try again.",
));
continue 'update;
}
Err(err) => {
tracing::error!(
event = "error",
context = "mailbox_set",
account_id = account_id,
error = ?err,
"Failed to update mailbox(es).");
return Err(MethodError::ServerPartialFail);
}
}
}
ctx.response.updated.append(id, None);
}
Err(err) => {
ctx.response.not_updated.append(id, err);
continue 'update;
}
}
} else {
ctx.response.not_updated.append(id, SetError::not_found());
}
}
// Process deletions
let mut did_remove_emails = false;
for id in ctx.will_destroy {
match self
.mailbox_destroy(
account_id,
id.document_id(),
&mut changes,
ctx.access_token,
on_destroy_remove_emails,
)
.await?
{
Ok(removed_emails) => {
did_remove_emails |= removed_emails;
ctx.response.destroyed.push(id);
}
Err(err) => {
ctx.response.not_destroyed.append(id, err);
}
}
}
// Write changes
if !changes.is_empty() {
let state_change =
StateChange::new(account_id).with_change(DataType::Mailbox, changes.change_id);
ctx.response.state_change = if did_remove_emails {
state_change
.with_change(DataType::Email, changes.change_id)
.with_change(DataType::Thread, changes.change_id)
} else {
state_change
}
.into();
ctx.response.new_state = Some(self.commit_changes(account_id, changes).await?.into());
}
Ok(ctx.response)
}
pub async fn mailbox_destroy(
&self,
account_id: u32,
document_id: u32,
changes: &mut ChangeLogBuilder,
access_token: &AccessToken,
remove_emails: bool,
) -> Result<Result<bool, SetError>, MethodError> {
// Internal folders cannot be deleted
#[cfg(feature = "test_mode")]
if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() {
return Ok(Err(SetError::forbidden().with_description(
"You are not allowed to delete Inbox, Junk or Trash folders.",
)));
}
#[cfg(not(feature = "test_mode"))]
if [INBOX_ID, TRASH_ID, JUNK_ID].contains(&document_id) && !access_token.is_super_user() {
return Ok(Err(SetError::forbidden().with_description(
"You are not allowed to delete Inbox, Junk or Trash folders.",
)));
}
// Verify that this mailbox does not have sub-mailboxes
if !self
.filter(
account_id,
Collection::Mailbox,
vec![Filter::eq(Property::ParentId, document_id + 1)],
)
.await?
.results
.is_empty()
{
return Ok(Err(SetError::new(SetErrorType::MailboxHasChild)
.with_description("Mailbox has at least one children.")));
}
// Verify that the mailbox is empty
let mut did_remove_emails = false;
if let Some(message_ids) = self
.get_tag(
account_id,
Collection::Email,
Property::MailboxIds,
document_id,
)
.await?
{
if remove_emails {
// Flag removal for state change notification
did_remove_emails = true;
// If the message is in multiple mailboxes, untag it from the current mailbox,
// otherwise delete it.
for message_id in message_ids {
// Obtain mailboxIds
if let Some(mailbox_ids) = self
.get_property::<HashedValue<Vec<UidMailbox>>>(
account_id,
Collection::Email,
message_id,
Property::MailboxIds,
)
.await?
.and_then(|mut ids| {
let idx = ids
.inner
.iter()
.position(|&id| id.mailbox_id == document_id)?;
ids.inner.swap_remove(idx);
Some(ids)
})
{
if !mailbox_ids.inner.is_empty() {
// Obtain threadId
if let Some(thread_id) = self
.get_property::<u32>(
account_id,
Collection::Email,
message_id,
Property::ThreadId,
)
.await?
{
// Untag message from mailbox
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Email)
.update_document(message_id)
.assert_value(Property::MailboxIds, &mailbox_ids)
.value(Property::MailboxIds, mailbox_ids.inner, F_VALUE)
.value(Property::MailboxIds, document_id, F_BITMAP | F_CLEAR);
match self.store.write(batch.build()).await {
Ok(_) => changes.log_update(
Collection::Email,
Id::from_parts(thread_id, message_id),
),
Err(store::Error::AssertValueFailed) => {
return Ok(Err(SetError::forbidden().with_description(
concat!(
"Another process modified a message in this mailbox ",
"while deleting it, please try again."
),
)));
}
Err(err) => {
tracing::error!(
event = "error",
context = "mailbox_set",
account_id = account_id,
mailbox_id = document_id,
message_id = message_id,
error = ?err,
"Failed to update message while deleting mailbox.");
return Err(MethodError::ServerPartialFail);
}
}
} else {
tracing::debug!(
event = "error",
context = "mailbox_set",
account_id = account_id,
mailbox_id = document_id,
message_id = message_id,
"Message does not have a threadId, skipping."
);
}
} else {
// Delete message
if let Ok(mut change) =
self.email_delete(account_id, message_id).await?
{
change.changes.remove(&(Collection::Mailbox as u8));
changes.merge(change);
}
}
} else {
tracing::debug!(
event = "error",
context = "mailbox_set",
account_id = account_id,
mailbox_id = document_id,
message_id = message_id,
"Message is not in the mailbox, skipping."
);
}
}
} else {
return Ok(Err(SetError::new(SetErrorType::MailboxHasEmail)
.with_description("Mailbox is not empty.")));
}
}
// Obtain mailbox
if let Some(mailbox) = self
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::Mailbox,
document_id,
Property::Value,
)
.await?
{
// Validate ACLs
if access_token.is_shared(account_id) {
let acl = mailbox.inner.effective_acl(access_token);
if !acl.contains(Acl::Administer) {
if !acl.contains(Acl::Delete) {
return Ok(Err(SetError::forbidden()
.with_description("You are not allowed to delete this mailbox.")));
} else if remove_emails && !acl.contains(Acl::RemoveItems) {
return Ok(Err(SetError::forbidden().with_description(
"You are not allowed to delete emails from this mailbox.",
)));
}
}
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox)
.delete_document(document_id)
.value(Property::EmailIds, (), F_VALUE | F_CLEAR)
.custom(ObjectIndexBuilder::new(SCHEMA).with_current(mailbox));
match self.store.write(batch.build()).await {
Ok(_) => {
changes.log_delete(Collection::Mailbox, document_id);
Ok(Ok(did_remove_emails))
}
Err(store::Error::AssertValueFailed) => Ok(Err(SetError::forbidden()
.with_description(concat!(
"Another process modified this mailbox ",
"while deleting it, please try again."
)))),
Err(err) => {
tracing::error!(
event = "error",
context = "mailbox_set",
account_id = account_id,
document_id = document_id,
error = ?err,
"Failed to delete mailbox.");
Err(MethodError::ServerPartialFail)
}
}
} else {
Ok(Err(SetError::not_found()))
}
}
#[allow(clippy::blocks_in_conditions)]
async fn mailbox_set_item(
&self,
changes_: Object<SetValue>,
update: Option<(u32, HashedValue<Object<Value>>)>,
ctx: &SetContext<'_>,
) -> Result<Result<ObjectIndexBuilder, SetError>, MethodError> {
// Parse properties
let mut changes = Object::with_capacity(changes_.properties.len());
for (property, value) in changes_.properties {
let value = match ctx.response.eval_object_references(value) {
Ok(value) => value,
Err(err) => {
return Ok(Err(err));
}
};
let value = match (&property, value) {
(Property::Name, MaybePatchValue::Value(Value::Text(value))) => {
let value = value.trim();
if !value.is_empty() && value.len() < self.config.mailbox_name_max_len {
Value::Text(value.to_string())
} else {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Name)
.with_description(
if !value.is_empty() {
"Mailbox name is too long."
} else {
"Mailbox name cannot be empty."
}
.to_string(),
)));
}
}
(Property::ParentId, MaybePatchValue::Value(Value::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.")));
}
Value::Id((parent_id + 1).into())
}
(Property::ParentId, MaybePatchValue::Value(Value::Null)) => Value::Id(0u64.into()),
(Property::IsSubscribed, MaybePatchValue::Value(Value::Bool(subscribe))) => {
if let Some((_, current_fields)) = update.as_ref() {
if let Some(value) = current_fields
.inner
.mailbox_subscribe(ctx.access_token.primary_id(), subscribe)
{
value
} else {
continue;
}
} else if subscribe {
Value::List(vec![Value::Id(ctx.access_token.primary_id().into())])
} else {
continue;
}
}
(Property::Role, MaybePatchValue::Value(Value::Text(value))) => {
let role = value.trim().to_lowercase();
if [
"inbox", "trash", "spam", "junk", "drafts", "archive", "sent",
]
.contains(&role.as_str())
{
Value::Text(role)
} else {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Role)
.with_description(format!("Invalid role {role:?}."))));
}
}
(Property::Role, MaybePatchValue::Value(Value::Null)) => Value::Null,
(Property::SortOrder, MaybePatchValue::Value(Value::UnsignedInt(value))) => {
Value::UnsignedInt(value)
}
(Property::Acl, value) => {
match self
.acl_set(&mut changes, update.as_ref().map(|(_, obj)| obj), value)
.await
{
Ok(_) => continue,
Err(err) => {
return Ok(Err(err));
}
}
}
_ => {
return Ok(Err(SetError::invalid_properties()
.with_property(property)
.with_description("Invalid property or value.".to_string())))
}
};
changes.append(property, value);
}
// Validate depth and circular parent-child relationship
if let Value::Id(mailbox_parent_id) = changes.get(&Property::ParentId) {
let current_mailbox_id = update
.as_ref()
.map_or(u32::MAX, |(mailbox_id, _)| *mailbox_id + 1);
let mut mailbox_parent_id = mailbox_parent_id.document_id();
let mut success = false;
for depth in 0..self.config.mailbox_max_depth {
if mailbox_parent_id == current_mailbox_id {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::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(mut fields) = self
.get_property::<Object<Value>>(
ctx.account_id,
Collection::Mailbox,
parent_document_id,
Property::Value,
)
.await?
{
if depth == 0
&& ctx.is_shared
&& !fields
.effective_acl(ctx.access_token)
.contains_any([Acl::CreateChild, Acl::Administer].into_iter())
{
return Ok(Err(SetError::forbidden().with_description(
"You are not allowed to create sub mailboxes under this mailbox.",
)));
}
mailbox_parent_id = fields
.properties
.remove(&Property::ParentId)
.and_then(|v| v.try_unwrap_id().map(|id| id.document_id()))
.unwrap_or(0);
} 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(Property::ParentId)
.with_description("Mailbox parent does not exist.")));
}
}
if !success {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::ParentId)
.with_description(
"Mailbox parent-child relationship is too deep.",
)));
}
} else if update.is_none() {
// Set parentId if the field is missing
changes.append(Property::ParentId, Value::Id(0u64.into()));
}
// Generate IMAP UID validity
if update.is_none() {
changes.append(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
);
}
// Verify that the mailbox role is unique.
if let Value::Text(mailbox_role) = changes.get(&Property::Role) {
if update
.as_ref()
.map(|(_, update)| update.inner.get(&Property::Role))
.and_then(|v| v.as_string())
.unwrap_or_default()
!= mailbox_role
{
if !self
.filter(
ctx.account_id,
Collection::Mailbox,
vec![Filter::eq(Property::Role, mailbox_role.as_str())],
)
.await?
.results
.is_empty()
{
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Role)
.with_description(format!(
"A mailbox with role '{}' already exists.",
mailbox_role
))));
}
// Role of internal folders cannot be modified
if update.as_ref().map_or(false, |(document_id, _)| {
*document_id == INBOX_ID || *document_id == TRASH_ID
}) {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Role)
.with_description(
"You are not allowed to change the role of Inbox or Trash folders.",
)));
}
}
}
// Verify that the mailbox name is unique.
if let Value::Text(mailbox_name) = changes.get(&Property::Name) {
// Obtain parent mailbox id
if let Some(parent_mailbox_id) = if let Some(mailbox_parent_id) = &changes
.properties
.get(&Property::ParentId)
.and_then(|id| id.as_id().map(|id| id.document_id()))
{
(*mailbox_parent_id).into()
} else if let Some((_, current_fields)) = &update {
if current_fields
.inner
.properties
.get(&Property::Name)
.and_then(|n| n.as_string())
!= Some(mailbox_name)
{
current_fields
.inner
.properties
.get(&Property::ParentId)
.and_then(|id| id.as_id().map(|id| id.document_id()))
.unwrap_or_default()
.into()
} else {
None
}
} else {
0.into()
} {
if !self
.filter(
ctx.account_id,
Collection::Mailbox,
vec![
Filter::eq(Property::Name, mailbox_name.as_str()),
Filter::eq(Property::ParentId, parent_mailbox_id),
],
)
.await?
.results
.is_empty()
{
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Name)
.with_description(format!(
"A mailbox with name '{}' already exists.",
mailbox_name
))));
}
}
}
// Refresh ACLs
let current = update.map(|(_, current)| current);
if changes.properties.contains_key(&Property::Acl) {
self.refresh_acls(&changes, &current);
}
// Validate
Ok(ObjectIndexBuilder::new(SCHEMA)
.with_changes(changes)
.with_current_opt(current)
.validate())
}
pub async fn mailbox_get_or_create(
&self,
account_id: u32,
) -> Result<RoaringBitmap, MethodError> {
let mut mailbox_ids = self
.get_document_ids(account_id, Collection::Mailbox)
.await?
.unwrap_or_default();
if !mailbox_ids.is_empty() {
return Ok(mailbox_ids);
}
#[cfg(feature = "test_mode")]
if mailbox_ids.is_empty() && account_id == 0 {
return Ok(mailbox_ids);
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
// Create mailboxes
for (name, role) in [
("Inbox", "inbox"),
("Deleted Items", "trash"),
("Junk Mail", "junk"),
("Drafts", "drafts"),
("Sent Items", "sent"),
] {
let mailbox_id = self
.assign_document_id(account_id, Collection::Mailbox)
.await?;
batch.create_document(mailbox_id).custom(
ObjectIndexBuilder::new(SCHEMA).with_changes(
Object::with_capacity(4)
.with_property(Property::Name, name)
.with_property(Property::Role, role)
.with_property(Property::ParentId, Value::Id(0u64.into()))
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
),
),
);
mailbox_ids.insert(mailbox_id);
}
self.store.write(batch.build()).await.map_err(|err| {
tracing::error!(
event = "error",
context = "mailbox_get_or_create",
error = ?err,
"Failed to create mailboxes.");
MethodError::ServerPartialFail
})?;
Ok(mailbox_ids)
}
pub async fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
) -> Result<Option<(u32, Option<u64>)>, MethodError> {
let expanded_path =
if let Some(expand_path) = self.mailbox_expand_path(account_id, path, false).await? {
expand_path
} else {
return Ok(None);
};
let mut next_parent_id = 0;
let mut path = expanded_path.path.into_iter().peekable();
'outer: while let Some(name) = path.peek() {
for (part, parent_id, document_id) in &expanded_path.found_names {
if part.eq(name) && *parent_id == next_parent_id {
next_parent_id = *document_id;
path.next();
continue 'outer;
}
}
break;
}
// Create missing folders
if path.peek().is_some() {
let mut batch = BatchBuilder::new();
let mut changes = self.begin_changes(account_id).await?;
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
for name in path {
if name.len() > self.config.mailbox_name_max_len {
return Ok(None);
}
let document_id = self
.assign_document_id(account_id, Collection::Mailbox)
.await?;
batch.create_document(document_id).custom(
ObjectIndexBuilder::new(SCHEMA).with_changes(
Object::with_capacity(3)
.with_property(Property::Name, name)
.with_property(Property::ParentId, Value::Id(Id::from(next_parent_id)))
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
),
),
);
changes.log_insert(Collection::Mailbox, document_id);
next_parent_id = document_id + 1;
}
let change_id = changes.change_id;
batch.custom(changes);
self.write_batch(batch).await?;
Ok(Some((next_parent_id - 1, Some(change_id))))
} else {
Ok(Some((next_parent_id - 1, None)))
}
}
}
pub trait MailboxSubscribe {
fn mailbox_subscribe(&self, account_id: u32, subscribed: bool) -> Option<Value>;
}
impl MailboxSubscribe for Object<Value> {
fn mailbox_subscribe(&self, account_id: u32, subscribe: bool) -> Option<Value> {
let account_id = Value::Id(account_id.into());
if let Value::List(subscriptions) = self.get(&Property::IsSubscribed) {
if subscribe {
if !subscriptions.contains(&account_id) {
let mut current_subscriptions = subscriptions.clone();
current_subscriptions.push(account_id);
Value::List(current_subscriptions).into()
} else {
None
}
} else if subscriptions.contains(&account_id) {
if subscriptions.len() > 1 {
Value::List(
subscriptions
.iter()
.filter(|id| *id != &account_id)
.cloned()
.collect(),
)
.into()
} else {
Value::Null.into()
}
} else {
None
}
} else if subscribe {
Value::List(vec![account_id]).into()
} else {
None
}
}
}