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:
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{ObjectId, create::Arguments, list::Attribute},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::{Permission, StorageQuota};
|
||||
use std::time::Instant;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::Collection, id::Id, special_use::SpecialUse};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCreate)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let is_utf8 = self.is_utf8;
|
||||
let is_objectid = self.is_objectid;
|
||||
|
||||
spawn_op!(data, {
|
||||
for request in requests {
|
||||
match request.parse_create(is_utf8) {
|
||||
Ok(argument) => match data.create_folder(argument, is_objectid).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(response.into_bytes()).await?;
|
||||
}
|
||||
Err(error) => {
|
||||
data.write_error(error).await?;
|
||||
}
|
||||
},
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn create_folder(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
is_objectid: bool,
|
||||
) -> trc::Result<StatusResponse> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox name
|
||||
let params = self
|
||||
.validate_mailbox_create(&arguments.mailbox_name, arguments.mailbox_role)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
debug_assert!(!params.path.is_empty());
|
||||
|
||||
// Validate quota
|
||||
let account = self
|
||||
.server
|
||||
.account(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mailbox_count = self
|
||||
.server
|
||||
.get_cached_messages(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.mailboxes
|
||||
.items
|
||||
.len();
|
||||
if mailbox_count + params.path.len()
|
||||
> self
|
||||
.server
|
||||
.object_quota(account.object_quotas(), StorageQuota::MaxMailboxes)
|
||||
as usize
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"There are too many mailboxes, ",
|
||||
"please delete some before adding a new one."
|
||||
))
|
||||
.code(ResponseCode::OverQuota)
|
||||
.id(arguments.tag.clone()));
|
||||
}
|
||||
|
||||
// Build batch
|
||||
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());
|
||||
let mut next_document_id = self
|
||||
.server
|
||||
.store()
|
||||
.assign_document_ids(
|
||||
params.account_id,
|
||||
Collection::Mailbox,
|
||||
params.path.len() as u64,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (pos, &path_item) in params.path.iter().enumerate() {
|
||||
let mut mailbox = email::mailbox::Mailbox::new(path_item).with_parent_id(parent_id);
|
||||
|
||||
if pos == params.path.len() - 1
|
||||
&& let Some(mailbox_role) = arguments.mailbox_role.map(attr_to_role)
|
||||
{
|
||||
mailbox.role = mailbox_role;
|
||||
}
|
||||
let mailbox_id = next_document_id;
|
||||
next_document_id -= 1;
|
||||
batch
|
||||
.with_account_id(params.account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(mailbox))
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.commit_point();
|
||||
parent_id = mailbox_id + 1;
|
||||
create_ids.push(mailbox_id);
|
||||
}
|
||||
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::CreateMailbox),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = params.account_id,
|
||||
MailboxId = create_ids
|
||||
.iter()
|
||||
.map(|&id| trc::Value::from(id))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
let response = StatusResponse::ok("Mailbox created.").with_tag(arguments.tag);
|
||||
Ok(if is_objectid {
|
||||
response.with_code(ResponseCode::ObjectId(ObjectId {
|
||||
mailbox_id: Some(Id::from(parent_id - 1)),
|
||||
account_id: Some(Id::from(params.account_id)),
|
||||
..Default::default()
|
||||
}))
|
||||
} else {
|
||||
response
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn validate_mailbox_create<'x>(
|
||||
&self,
|
||||
mailbox_name: &'x str,
|
||||
mailbox_role: Option<Attribute>,
|
||||
) -> trc::Result<CreateParams<'x>> {
|
||||
// Remove leading and trailing separators
|
||||
let mut name = mailbox_name.trim();
|
||||
if let Some(suffix) = name.strip_prefix('/') {
|
||||
name = suffix.trim();
|
||||
};
|
||||
if let Some(prefix) = name.strip_suffix('/') {
|
||||
name = prefix.trim();
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Invalid folder name '{mailbox_name}'.",)));
|
||||
}
|
||||
|
||||
// Build path
|
||||
let mut path = Vec::new();
|
||||
if name.contains('/') {
|
||||
// Locate parent mailbox
|
||||
for path_item in name.split('/') {
|
||||
let path_item = path_item.trim();
|
||||
if path_item.is_empty() {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Invalid empty path item."));
|
||||
} else if path_item.len() > self.server.core.email.mailbox_name_max_len {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox name is too long."));
|
||||
}
|
||||
path.push(path_item);
|
||||
}
|
||||
|
||||
if path.len() > self.server.core.email.mailbox_max_depth {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox path is too deep."));
|
||||
}
|
||||
} else {
|
||||
path.push(name);
|
||||
}
|
||||
|
||||
// Validate special folders
|
||||
let mut parent_mailbox_id = None;
|
||||
let mut parent_mailbox_name = None;
|
||||
let (account_id, path) = {
|
||||
let mailboxes = self.mailboxes.lock();
|
||||
let (account, full_path, prefix) =
|
||||
if path.first() == Some(&self.server.core.email.shared_folder.as_str()) {
|
||||
// Shared Folders/<username>/<folder>
|
||||
if path.len() < 3 {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailboxes under root shared folders are not allowed.")
|
||||
.code(ResponseCode::Cannot));
|
||||
}
|
||||
|
||||
// Build path
|
||||
let root = &mut path[2];
|
||||
if root.eq_ignore_ascii_case("INBOX") {
|
||||
*root = "INBOX";
|
||||
}
|
||||
let full_path = path.join("/");
|
||||
let prefix = Some(format!("{}/{}", path[0], path[1]));
|
||||
|
||||
// Locate account
|
||||
if let Some(account) = mailboxes
|
||||
.iter()
|
||||
.skip(1)
|
||||
.find(|account| account.prefix == prefix)
|
||||
{
|
||||
(account, full_path, prefix)
|
||||
} else {
|
||||
#[allow(clippy::unnecessary_literal_unwrap)]
|
||||
return Err(trc::ImapEvent::Error.into_err().details(format!(
|
||||
"Shared account '{}' not found.",
|
||||
prefix.unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
} else if let Some(account) = mailboxes.first() {
|
||||
let root = &mut path[0];
|
||||
if root.eq_ignore_ascii_case("INBOX") {
|
||||
*root = "INBOX";
|
||||
}
|
||||
|
||||
(account, path.join("/"), None)
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Internal server error.")
|
||||
.caused_by(trc::location!())
|
||||
.code(ResponseCode::ContactAdmin));
|
||||
};
|
||||
|
||||
// Locate parent mailbox
|
||||
if account.mailbox_names.contains_key(&full_path) {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Mailbox '{}' already exists.", full_path))
|
||||
.code(ResponseCode::AlreadyExists));
|
||||
}
|
||||
|
||||
(
|
||||
account.account_id,
|
||||
if path.len() > 1 {
|
||||
let mut create_path = Vec::with_capacity(path.len());
|
||||
while !path.is_empty() {
|
||||
let mailbox_name: String = path.join("/");
|
||||
if let Some(&mailbox_id) = account.mailbox_names.get(&mailbox_name) {
|
||||
parent_mailbox_id = mailbox_id.into();
|
||||
parent_mailbox_name = mailbox_name.into();
|
||||
break;
|
||||
} else if prefix
|
||||
.as_ref()
|
||||
.is_some_and(|prefix| prefix == &mailbox_name)
|
||||
{
|
||||
break;
|
||||
} else {
|
||||
create_path.push(path.pop().unwrap());
|
||||
}
|
||||
}
|
||||
create_path.reverse();
|
||||
create_path
|
||||
} else {
|
||||
path
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
// Validate ACLs
|
||||
if let Some(parent_mailbox_id) = parent_mailbox_id {
|
||||
if !self
|
||||
.check_mailbox_acl(account_id, parent_mailbox_id, Acl::CreateChild)
|
||||
.await?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You are not allowed to create sub mailboxes under this mailbox.")
|
||||
.code(ResponseCode::NoPerm));
|
||||
}
|
||||
} else if self.account_id != account_id
|
||||
&& !self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.is_member(account_id)
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You are not allowed to create root folders under shared folders.")
|
||||
.code(ResponseCode::Cannot));
|
||||
}
|
||||
|
||||
Ok(CreateParams {
|
||||
account_id,
|
||||
path,
|
||||
parent_mailbox_id,
|
||||
parent_mailbox_name,
|
||||
special_use: if let Some(mailbox_role) = mailbox_role {
|
||||
// Make sure role is unique
|
||||
let special_use = attr_to_role(mailbox_role);
|
||||
if self
|
||||
.server
|
||||
.get_cached_messages(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.mailbox_by_role(&special_use)
|
||||
.is_some()
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!(
|
||||
"A mailbox with role '{}' already exists.",
|
||||
special_use.as_str().unwrap_or_default()
|
||||
))
|
||||
.code(ResponseCode::UseAttr));
|
||||
}
|
||||
Some(mailbox_role)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_rename: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateParams<'x> {
|
||||
pub account_id: u32,
|
||||
pub path: Vec<&'x str>,
|
||||
pub parent_mailbox_id: Option<u32>,
|
||||
pub parent_mailbox_name: Option<String>,
|
||||
pub special_use: Option<Attribute>,
|
||||
pub is_rename: bool,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn attr_to_role(attr: Attribute) -> SpecialUse {
|
||||
match attr {
|
||||
Attribute::Archive => SpecialUse::Archive,
|
||||
Attribute::Drafts => SpecialUse::Drafts,
|
||||
Attribute::Junk => SpecialUse::Junk,
|
||||
Attribute::Sent => SpecialUse::Sent,
|
||||
Attribute::Trash => SpecialUse::Trash,
|
||||
Attribute::Important => SpecialUse::Important,
|
||||
Attribute::Memos => SpecialUse::Memos,
|
||||
Attribute::Scheduled => SpecialUse::Scheduled,
|
||||
Attribute::Snoozed => SpecialUse::Snoozed,
|
||||
_ => SpecialUse::None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user