/* * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use std::{collections::BTreeMap, time::Instant}; use crate::{ core::{Session, SessionData}, spawn_op, }; use common::listener::SessionStream; use directory::Permission; use imap_proto::{ protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, }; use jmap::{ auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA, services::state::StateManager, JmapMethods, }; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange, type_state::DataType, value::Value, }, }; use store::write::{assert::HashedValue, BatchBuilder}; use trc::AddContext; use super::ImapContext; impl Session { pub async fn handle_rename(&mut self, request: Request) -> trc::Result<()> { // Validate access self.assert_has_permission(Permission::ImapRename)?; let op_start = Instant::now(); let arguments = request.parse_rename(self.version)?; let data = self.state.session_data(); spawn_op!(data, { let response = data.rename_folder(arguments, op_start).await?; data.write_bytes(response.into_bytes()).await }) } } impl SessionData { pub async fn rename_folder( &self, arguments: Arguments, op_start: Instant, ) -> trc::Result { // Refresh mailboxes self.synchronize_mailboxes(false) .await .imap_ctx(&arguments.tag, trc::location!())?; // Validate mailbox name let mut params = self .validate_mailbox_create(&arguments.new_mailbox_name, None) .await .add_context(|err| err.id(arguments.tag.clone()))?; params.is_rename = true; // Validate source mailbox let mailbox_id = { let mut mailbox_id = None; for account in self.mailboxes.lock().iter() { if let Some(mailbox_id_) = account.mailbox_names.get(&arguments.mailbox_name) { if account.account_id == params.account_id { mailbox_id = (*mailbox_id_).into(); break; } else { return Err(trc::ImapEvent::Error .into_err() .details("Cannot move mailboxes between accounts.") .code(ResponseCode::Cannot) .id(arguments.tag)); } } } if let Some(mailbox_id) = mailbox_id { mailbox_id } else { return Err(trc::ImapEvent::Error .into_err() .details(format!("Mailbox '{}' not found.", arguments.mailbox_name)) .code(ResponseCode::NonExistent) .id(arguments.tag)); } }; // Obtain mailbox let mailbox = self .server .get_property::>>( params.account_id, Collection::Mailbox, mailbox_id, Property::Value, ) .await .imap_ctx(&arguments.tag, trc::location!())? .ok_or_else(|| { trc::ImapEvent::Error .into_err() .details(format!("Mailbox '{}' not found.", arguments.mailbox_name)) .caused_by(trc::location!()) .code(ResponseCode::NonExistent) .id(arguments.tag.clone()) })?; // Validate ACL let access_token = self .get_access_token() .await .imap_ctx(&arguments.tag, trc::location!())?; if access_token.is_shared(params.account_id) && !mailbox .inner .effective_acl(&access_token) .contains(Acl::Modify) { return Err(trc::ImapEvent::Error .into_err() .details("You are not allowed to rename this mailbox.") .code(ResponseCode::NoPerm) .id(arguments.tag)); } // Get new mailbox name from path let new_mailbox_name = params.path.pop().unwrap(); // Build batch let mut changes = self .server .begin_changes(params.account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; let mut parent_id = params.parent_mailbox_id.map(|id| id + 1).unwrap_or(0); let mut create_ids = Vec::with_capacity(params.path.len()); for &path_item in params.path.iter() { let mut batch = BatchBuilder::new(); batch .with_account_id(params.account_id) .with_collection(Collection::Mailbox) .create_document() .custom( ObjectIndexBuilder::new(SCHEMA).with_changes( Object::with_capacity(3) .with_property(Property::Name, path_item) .with_property(Property::ParentId, Value::Id(Id::from(parent_id))) .with_property( Property::Cid, Value::UnsignedInt(rand::random::() as u64), ), ), ); let mailbox_id = self .server .write_batch_expect_id(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; changes.log_insert(Collection::Mailbox, mailbox_id); parent_id = mailbox_id + 1; create_ids.push(mailbox_id); } let mut batch = BatchBuilder::new(); batch .with_account_id(params.account_id) .with_collection(Collection::Mailbox) .update_document(mailbox_id) .custom( ObjectIndexBuilder::new(SCHEMA) .with_current(mailbox) .with_changes( Object::with_capacity(3) .with_property(Property::Name, new_mailbox_name) .with_property(Property::ParentId, Value::Id(Id::from(parent_id))) .with_property( Property::Cid, Value::UnsignedInt(rand::random::() as u64), ), ), ); changes.log_update(Collection::Mailbox, mailbox_id); let change_id = changes.change_id; batch.custom(changes); self.server .write_batch(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; // Broadcast changes self.server .broadcast_state_change( StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id), ) .await; let mut mailboxes = if !create_ids.is_empty() { self.add_created_mailboxes(&mut params, change_id, create_ids) .add_context(|err| err.id(arguments.tag.clone()))? } else { self.mailboxes.lock() }; // Rename mailbox cache for account in mailboxes.iter_mut() { if account.account_id == params.account_id { // Update state account.state_mailbox = change_id.into(); // Update parents if arguments.mailbox_name.contains('/') { let mut parent_path = arguments.mailbox_name.split('/').collect::>(); parent_path.pop(); let parent_path = parent_path.join("/"); if let Some(old_parent_id) = account.mailbox_names.get(&parent_path) { if let Some(old_parent) = account.mailbox_state.get_mut(old_parent_id) { let prefix = format!("{}/", parent_path); old_parent.has_children = account.mailbox_names.keys().any(|name| { name != &arguments.mailbox_name && name.starts_with(&prefix) }); } } } if let Some(parent_mailbox) = params .parent_mailbox_id .and_then(|id| account.mailbox_state.get_mut(&id)) { parent_mailbox.has_children = true; } let prefix = format!("{}/", arguments.mailbox_name); let mut new_mailbox_names = BTreeMap::new(); for (mailbox_name, mailbox_id) in std::mem::take(&mut account.mailbox_names) { if mailbox_name != arguments.mailbox_name { if let Some(child_name) = mailbox_name.strip_prefix(&prefix) { new_mailbox_names .insert(format!("{}/{}", params.full_path, child_name), mailbox_id); } else { new_mailbox_names.insert(mailbox_name, mailbox_id); } } } new_mailbox_names.insert(params.full_path, mailbox_id); account.mailbox_names = new_mailbox_names; break; } } trc::event!( Imap(trc::ImapEvent::RenameMailbox), SpanId = self.session_id, AccountId = params.account_id, MailboxName = arguments.new_mailbox_name, MailboxId = mailbox_id, Elapsed = op_start.elapsed() ); Ok(StatusResponse::completed(Command::Rename).with_tag(arguments.tag)) } }