Skip to main content

stalwart_lib/jmap/src/mailbox/
set.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use crate::common::{
8    Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
9};
10#[allow(unused_imports)]
11use crate::email::mailbox::{INBOX_ID, JUNK_ID, TRASH_ID, UidMailbox};
12use crate::email::{
13    cache::{MessageCacheFetch, mailbox::MailboxCacheAccess},
14    mailbox::{
15        Mailbox,
16        destroy::{MailboxDestroy, MailboxDestroyError},
17    },
18};
19use crate::jmap::{
20    api::acl::{JmapAcl, JmapRights},
21    changes::state::JmapCacheState,
22};
23use crate::jmap_proto::{
24    error::set::{SetError, SetErrorType},
25    method::set::{SetRequest, SetResponse},
26    object::mailbox::{self, MailboxProperty, MailboxValue},
27    references::resolve::ResolveCreatedReference,
28    request::IntoValid,
29    types::state::State,
30};
31use crate::store::{
32    ValueKey,
33    roaring::RoaringBitmap,
34    write::{AlignedBytes, Archive, BatchBuilder, assert::AssertValue},
35};
36use crate::trc::AddContext;
37use crate::types::{
38    acl::Acl, collection::Collection, field::MailboxField, id::Id, special_use::SpecialUse,
39};
40use jmap_tools::{JsonPointerItem, Key, Map, Value};
41use std::future::Future;
42
43pub struct SetContext<'x> {
44    account_id: u32,
45    access_token: &'x AccessToken,
46    is_shared: bool,
47    response: SetResponse<mailbox::Mailbox>,
48    mailbox_ids: RoaringBitmap,
49    will_destroy: Vec<Id>,
50}
51
52pub trait MailboxSet: Sync + Send {
53    fn mailbox_set(
54        &self,
55        request: SetRequest<'_, mailbox::Mailbox>,
56        access_token: &AccessToken,
57    ) -> impl Future<Output = crate::trc::Result<SetResponse<mailbox::Mailbox>>> + Send;
58
59    fn mailbox_set_item(
60        &self,
61        changes_: Map<'_, MailboxProperty, MailboxValue>,
62        update: Option<(u32, Archive<Mailbox>)>,
63        ctx: &SetContext,
64    ) -> impl Future<
65        Output = crate::trc::Result<
66            Result<ObjectIndexBuilder<Mailbox, Mailbox>, SetError<MailboxProperty>>,
67        >,
68    > + Send;
69}
70
71impl MailboxSet for Server {
72    #[allow(clippy::blocks_in_conditions)]
73    async fn mailbox_set(
74        &self,
75        mut request: SetRequest<'_, mailbox::Mailbox>,
76        access_token: &AccessToken,
77    ) -> crate::trc::Result<SetResponse<mailbox::Mailbox>> {
78        // Prepare response
79        let account_id = request.account_id.document_id();
80        let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false);
81        let cache = self.get_cached_messages(account_id).await?;
82        let mut ctx = SetContext {
83            account_id,
84            is_shared: access_token.is_shared(account_id),
85            access_token,
86            response: SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
87                .with_state(cache.assert_state(true, &request.if_in_state)?),
88            mailbox_ids: RoaringBitmap::from_iter(cache.mailboxes.index.keys()),
89            will_destroy: request.unwrap_destroy().into_valid().collect(),
90        };
91        let mut change_id = None;
92
93        // Process creates
94        let mut batch = BatchBuilder::new();
95        'create: for (id, object) in request.unwrap_create() {
96            let Some(object) = object.into_object() else {
97                continue;
98            };
99
100            // Validate quota
101            if ctx.mailbox_ids.len() >= access_token.object_quota(Collection::Mailbox) as u64 {
102                ctx.response.not_created.append(
103                    id,
104                    SetError::new(SetErrorType::OverQuota).with_description(concat!(
105                        "There are too many mailboxes, ",
106                        "please delete some before adding a new one."
107                    )),
108                );
109                continue 'create;
110            }
111
112            match self.mailbox_set_item(object, None, &ctx).await? {
113                Ok(builder) => {
114                    batch
115                        .with_account_id(account_id)
116                        .with_collection(Collection::Mailbox);
117
118                    let parent_id = builder.changes().unwrap().parent_id;
119                    if parent_id > 0 {
120                        batch
121                            .with_document(parent_id - 1)
122                            .assert_value(MailboxField::Archive, AssertValue::Some);
123                    }
124
125                    let document_id = self
126                        .store()
127                        .assign_document_ids(account_id, Collection::Mailbox, 1)
128                        .await
129                        .caused_by(crate::trc::location!())?;
130
131                    batch
132                        .with_document(document_id)
133                        .custom(builder)
134                        .caused_by(crate::trc::location!())?
135                        .commit_point();
136
137                    ctx.mailbox_ids.insert(document_id);
138                    ctx.response.created(id, document_id);
139                }
140                Err(err) => {
141                    ctx.response.not_created.append(id, err);
142                    continue 'create;
143                }
144            }
145        }
146
147        if !batch.is_empty() {
148            change_id = self
149                .commit_batch(batch)
150                .await
151                .and_then(|ids| ids.last_change_id(account_id))
152                .caused_by(crate::trc::location!())?
153                .into();
154        }
155
156        // Process updates
157        let mut will_update = Vec::with_capacity(request.update.as_ref().map_or(0, |u| u.len()));
158        let mut batch = BatchBuilder::new();
159        'update: for (id, object) in request.unwrap_update().into_valid() {
160            // Make sure id won't be destroyed
161            if ctx.will_destroy.contains(&id) {
162                ctx.response
163                    .not_updated
164                    .append(id, SetError::will_destroy());
165                continue 'update;
166            }
167            let Some(object) = object.into_object() else {
168                continue 'update;
169            };
170
171            // Obtain mailbox
172            let document_id = id.document_id();
173            if let Some(mailbox) = self
174                .store()
175                .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
176                    account_id,
177                    Collection::Mailbox,
178                    document_id,
179                ))
180                .await?
181            {
182                // Validate ACL
183                let mailbox = mailbox
184                    .into_deserialized::<crate::email::mailbox::Mailbox>()
185                    .caused_by(crate::trc::location!())?;
186                if ctx.is_shared {
187                    let acl = mailbox.inner.acls.effective_acl(access_token);
188                    if !acl.contains(Acl::Modify) {
189                        ctx.response.not_updated.append(
190                            id,
191                            SetError::forbidden()
192                                .with_description("You are not allowed to modify this mailbox."),
193                        );
194                        continue 'update;
195                    } else if object.contains_key(&Key::Property(MailboxProperty::ShareWith))
196                        && !acl.contains(Acl::Share)
197                    {
198                        ctx.response.not_updated.append(
199                            id,
200                            SetError::forbidden().with_description(
201                                "You are not allowed to change the permissions of this mailbox.",
202                            ),
203                        );
204                        continue 'update;
205                    }
206                }
207
208                match self
209                    .mailbox_set_item(object, (document_id, mailbox).into(), &ctx)
210                    .await?
211                {
212                    Ok(builder) => {
213                        batch
214                            .with_account_id(account_id)
215                            .with_collection(Collection::Mailbox);
216
217                        let parent_id = builder.changes().unwrap().parent_id;
218                        if parent_id > 0 {
219                            batch
220                                .with_document(parent_id - 1)
221                                .assert_value(MailboxField::Archive, AssertValue::Some);
222                        }
223
224                        batch
225                            .with_document(document_id)
226                            .custom(builder)
227                            .caused_by(crate::trc::location!())?
228                            .commit_point();
229                        will_update.push(id);
230                    }
231                    Err(err) => {
232                        ctx.response.not_updated.append(id, err);
233                        continue 'update;
234                    }
235                }
236            } else {
237                ctx.response.not_updated.append(id, SetError::not_found());
238            }
239        }
240
241        if !batch.is_empty() {
242            match self
243                .commit_batch(batch)
244                .await
245                .and_then(|ids| ids.last_change_id(account_id))
246            {
247                Ok(change_id_) => {
248                    change_id = Some(change_id_);
249                    for id in will_update {
250                        ctx.response.updated.append(id, None);
251                    }
252                }
253                Err(err) if err.is_assertion_failure() => {
254                    for id in will_update {
255                        ctx.response.not_updated.append(
256                            id,
257                            SetError::forbidden().with_description(
258                                "Another process modified this mailbox, please try again.",
259                            ),
260                        );
261                    }
262                }
263                Err(err) => {
264                    return Err(err.caused_by(crate::trc::location!()));
265                }
266            }
267        }
268
269        // Process deletions
270        for id in ctx.will_destroy {
271            match self
272                .mailbox_destroy(
273                    account_id,
274                    id.document_id(),
275                    ctx.access_token,
276                    on_destroy_remove_emails,
277                )
278                .await?
279            {
280                Ok(change_id_) => {
281                    if change_id_.is_some() {
282                        change_id = change_id_;
283                    }
284                    ctx.response.destroyed.push(id);
285                }
286                Err(err) => {
287                    ctx.response.not_destroyed.append(
288                        id,
289                        match err {
290                            MailboxDestroyError::CannotDestroy => SetError::forbidden()
291                                .with_description(
292                                    "You are not allowed to delete Inbox, Junk or Trash folders.",
293                                ),
294                            MailboxDestroyError::Forbidden => SetError::forbidden()
295                                .with_description("You are not allowed to delete this mailbox."),
296                            MailboxDestroyError::HasChildren => {
297                                SetError::new(SetErrorType::MailboxHasChild)
298                                    .with_description("Mailbox has at least one children.")
299                            }
300                            MailboxDestroyError::HasEmails => {
301                                SetError::new(SetErrorType::MailboxHasEmail)
302                                    .with_description("Mailbox is not empty.")
303                            }
304                            MailboxDestroyError::NotFound => SetError::not_found(),
305                            MailboxDestroyError::AssertionFailed => SetError::forbidden()
306                                .with_description(concat!(
307                                    "Another process modified a message in this mailbox ",
308                                    "while deleting it, please try again."
309                                )),
310                        },
311                    );
312                }
313            }
314        }
315
316        // Write changes
317        if let Some(change_id) = change_id {
318            ctx.response.new_state = State::Exact(change_id).into();
319        }
320
321        Ok(ctx.response)
322    }
323
324    #[allow(clippy::blocks_in_conditions)]
325    async fn mailbox_set_item(
326        &self,
327        changes_: Map<'_, MailboxProperty, MailboxValue>,
328        update: Option<(u32, Archive<Mailbox>)>,
329        ctx: &SetContext<'_>,
330    ) -> crate::trc::Result<Result<ObjectIndexBuilder<Mailbox, Mailbox>, SetError<MailboxProperty>>>
331    {
332        // Parse properties
333        let mut changes = update
334            .as_ref()
335            .map(|(_, obj)| obj.inner.clone())
336            .unwrap_or_else(|| Mailbox::new(String::new()));
337        let mut has_acl_changes = false;
338        for (property, mut value) in changes_.into_vec() {
339            if let Err(err) = ctx.response.resolve_self_references(&mut value) {
340                return Ok(Err(err));
341            };
342            match (&property, value) {
343                (Key::Property(MailboxProperty::Name), Value::Str(value)) => {
344                    let value = value.trim();
345                    if !value.is_empty() && value.len() < self.core.jmap.mailbox_name_max_len {
346                        changes.name = value.into();
347                    } else {
348                        return Ok(Err(SetError::invalid_properties()
349                            .with_property(MailboxProperty::Name)
350                            .with_description(
351                                if !value.is_empty() {
352                                    "Mailbox name is too long."
353                                } else {
354                                    "Mailbox name cannot be empty."
355                                }
356                                .to_string(),
357                            )));
358                    }
359                }
360                (
361                    Key::Property(MailboxProperty::ParentId),
362                    Value::Element(MailboxValue::Id(value)),
363                ) => {
364                    let parent_id = value.document_id();
365                    if ctx.will_destroy.contains(&value) {
366                        return Ok(Err(SetError::will_destroy()
367                            .with_description("Parent ID will be destroyed.")));
368                    } else if !ctx.mailbox_ids.contains(parent_id) {
369                        return Ok(Err(SetError::invalid_properties()
370                            .with_description("Parent ID does not exist.")));
371                    }
372                    changes.parent_id = parent_id + 1;
373                }
374                (Key::Property(MailboxProperty::ParentId), Value::Null) => {
375                    changes.parent_id = 0;
376                }
377                (Key::Property(MailboxProperty::IsSubscribed), Value::Bool(subscribe)) => {
378                    let account_id = ctx.access_token.primary_id();
379                    if subscribe {
380                        if !changes.subscribers.contains(&account_id) {
381                            changes.subscribers.push(account_id);
382                        }
383                    } else {
384                        changes.subscribers.retain(|id| *id != account_id);
385                    }
386                }
387                (
388                    Key::Property(MailboxProperty::Role),
389                    Value::Element(MailboxValue::Role(role)),
390                ) => {
391                    changes.role = role;
392                }
393                (Key::Property(MailboxProperty::Role), Value::Null) => {
394                    changes.role = SpecialUse::None;
395                }
396                (Key::Property(MailboxProperty::SortOrder), Value::Number(value)) => {
397                    changes.sort_order = Some(value.cast_to_u64() as u32);
398                }
399                (Key::Property(MailboxProperty::ShareWith), value) => {
400                    match JmapRights::acl_set::<mailbox::Mailbox>(value) {
401                        Ok(acls) => {
402                            has_acl_changes = true;
403                            changes.acls = acls;
404                            continue;
405                        }
406                        Err(err) => {
407                            return Ok(Err(err));
408                        }
409                    }
410                }
411                (Key::Property(MailboxProperty::Pointer(pointer)), value)
412                    if matches!(
413                        pointer.first(),
414                        Some(JsonPointerItem::Key(Key::Property(
415                            MailboxProperty::ShareWith
416                        )))
417                    ) =>
418                {
419                    let mut pointer = pointer.iter();
420                    pointer.next();
421
422                    match JmapRights::acl_patch::<mailbox::Mailbox>(changes.acls, pointer, value) {
423                        Ok(acls) => {
424                            has_acl_changes = true;
425                            changes.acls = acls;
426                            continue;
427                        }
428                        Err(err) => {
429                            return Ok(Err(err));
430                        }
431                    }
432                }
433
434                _ => {
435                    return Ok(Err(SetError::invalid_properties()
436                        .with_property(property.into_owned())
437                        .with_description("Invalid property or value.".to_string())));
438                }
439            }
440        }
441
442        // Validate depth and circular parent-child relationship
443        if update
444            .as_ref()
445            .is_none_or(|(_, m)| m.inner.parent_id != changes.parent_id)
446        {
447            let mut mailbox_parent_id = changes.parent_id;
448            let current_mailbox_id = update
449                .as_ref()
450                .map_or(u32::MAX, |(mailbox_id, _)| *mailbox_id + 1);
451            let mut success = false;
452            for depth in 0..self.core.jmap.mailbox_max_depth {
453                if mailbox_parent_id == current_mailbox_id {
454                    return Ok(Err(SetError::invalid_properties()
455                        .with_property(MailboxProperty::ParentId)
456                        .with_description("Mailbox cannot be a parent of itself.")));
457                } else if mailbox_parent_id == 0 {
458                    if depth == 0 && ctx.is_shared {
459                        return Ok(Err(SetError::forbidden()
460                            .with_description("You are not allowed to create root folders.")));
461                    }
462                    success = true;
463                    break;
464                }
465                let parent_document_id = mailbox_parent_id - 1;
466
467                if let Some(mailbox_) = self
468                    .store()
469                    .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
470                        ctx.account_id,
471                        Collection::Mailbox,
472                        parent_document_id,
473                    ))
474                    .await?
475                {
476                    let mailbox = mailbox_
477                        .unarchive::<crate::email::mailbox::Mailbox>()
478                        .caused_by(crate::trc::location!())?;
479                    if depth == 0
480                        && ctx.is_shared
481                        && !mailbox
482                            .acls
483                            .effective_acl(ctx.access_token)
484                            .contains(Acl::CreateChild)
485                    {
486                        return Ok(Err(SetError::forbidden().with_description(
487                            "You are not allowed to create sub mailboxes under this mailbox.",
488                        )));
489                    }
490
491                    mailbox_parent_id = mailbox.parent_id.into();
492                } else if ctx.mailbox_ids.contains(parent_document_id) {
493                    // Parent mailbox is probably created within the same request
494                    success = true;
495                    break;
496                } else {
497                    return Ok(Err(SetError::invalid_properties()
498                        .with_property(MailboxProperty::ParentId)
499                        .with_description("Mailbox parent does not exist.")));
500                }
501            }
502
503            if !success {
504                return Ok(Err(SetError::invalid_properties()
505                    .with_property(MailboxProperty::ParentId)
506                    .with_description(
507                        "Mailbox parent-child relationship is too deep.",
508                    )));
509            }
510        }
511
512        let cached_mailboxes = self.get_cached_messages(ctx.account_id).await?;
513
514        // Verify that the mailbox role is unique.
515        if update
516            .as_ref()
517            .is_none_or(|(_, m)| m.inner.role != changes.role)
518        {
519            if !matches!(changes.role, SpecialUse::None)
520                && cached_mailboxes.mailbox_by_role(&changes.role).is_some()
521            {
522                return Ok(Err(SetError::invalid_properties()
523                    .with_property(MailboxProperty::Role)
524                    .with_description(format!(
525                        "A mailbox with role '{}' already exists.",
526                        changes.role.as_str().unwrap_or_default()
527                    ))));
528            }
529
530            // Role of internal folders cannot be modified
531            if update.as_ref().is_some_and(|(document_id, _)| {
532                *document_id == INBOX_ID || *document_id == TRASH_ID || *document_id == JUNK_ID
533            }) {
534                return Ok(Err(SetError::invalid_properties()
535                    .with_property(MailboxProperty::Role)
536                    .with_description(
537                        "You are not allowed to change the role of Inbox, Junk or Trash folders.",
538                    )));
539            }
540        }
541
542        // Verify that the mailbox name is unique.
543        if !changes.name.is_empty() {
544            // Obtain parent mailbox id
545            let lower_name = changes.name.to_lowercase();
546            if update
547                .as_ref()
548                .is_none_or(|(_, m)| m.inner.name != changes.name)
549                && cached_mailboxes.mailboxes.items.iter().any(|m| {
550                    m.name.to_lowercase() == lower_name
551                        && m.parent_id().map_or(0, |id| id + 1) == changes.parent_id
552                })
553            {
554                return Ok(Err(SetError::invalid_properties()
555                    .with_property(MailboxProperty::Name)
556                    .with_description(format!(
557                        "A mailbox with name '{}' already exists.",
558                        changes.name
559                    ))));
560            }
561        } else {
562            return Ok(Err(SetError::invalid_properties()
563                .with_property(MailboxProperty::Name)
564                .with_description("Mailbox name cannot be empty.")));
565        }
566
567        // Refresh ACLs
568        let current = update.map(|(_, current)| current);
569        if has_acl_changes {
570            if !changes.acls.is_empty()
571                && let Err(err) = self.acl_validate(&changes.acls).await
572            {
573                return Ok(Err(err.into()));
574            }
575
576            self.refresh_acls(
577                &changes.acls,
578                current.as_ref().map(|m| m.inner.acls.as_slice()),
579            )
580            .await;
581        }
582
583        // Validate
584        Ok(Ok(ObjectIndexBuilder::new()
585            .with_changes(changes)
586            .with_current_opt(current)))
587    }
588}