1use 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if !changes.name.is_empty() {
544 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 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 Ok(Ok(ObjectIndexBuilder::new()
585 .with_changes(changes)
586 .with_current_opt(current)))
587 }
588}