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.
273 lines
11 KiB
Rust
273 lines
11 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::{
|
|
changes::state::JmapCacheState,
|
|
email::{PatchResult, handle_email_patch, ingested_into_object},
|
|
};
|
|
use common::{Server, auth::AccessToken};
|
|
use email::{
|
|
cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess},
|
|
message::copy::{CopyMessageError, EmailCopy},
|
|
};
|
|
use http_proto::HttpSessionData;
|
|
use jmap_proto::{
|
|
error::set::SetError,
|
|
method::{
|
|
copy::{CopyRequest, CopyResponse},
|
|
set::SetRequest,
|
|
},
|
|
object::email::{Email, EmailProperty, EmailValue},
|
|
request::{
|
|
Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod,
|
|
method::{MethodFunction, MethodName, MethodObject},
|
|
reference::MaybeResultReference,
|
|
},
|
|
};
|
|
use jmap_tools::{Key, Value};
|
|
use std::future::Future;
|
|
use trc::AddContext;
|
|
use types::acl::Acl;
|
|
use utils::map::vec_map::VecMap;
|
|
|
|
pub trait JmapEmailCopy: Sync + Send {
|
|
fn email_copy<'x>(
|
|
&self,
|
|
request: CopyRequest<'x, Email>,
|
|
access_token: &AccessToken,
|
|
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
|
session: &HttpSessionData,
|
|
) -> impl Future<Output = trc::Result<CopyResponse<Email>>> + Send;
|
|
}
|
|
|
|
impl JmapEmailCopy for Server {
|
|
async fn email_copy<'x>(
|
|
&self,
|
|
request: CopyRequest<'x, Email>,
|
|
access_token: &AccessToken,
|
|
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
|
session: &HttpSessionData,
|
|
) -> trc::Result<CopyResponse<Email>> {
|
|
let account_id = request.account_id.document_id();
|
|
let from_account_id = request.from_account_id.document_id();
|
|
|
|
if account_id == from_account_id {
|
|
return Err(trc::JmapEvent::InvalidArguments
|
|
.into_err()
|
|
.details("From accountId is equal to fromAccountId"));
|
|
}
|
|
let cache = self.get_cached_messages(account_id).await?;
|
|
let old_state = cache.assert_state(false, &request.if_in_state)?;
|
|
let mut response = CopyResponse {
|
|
from_account_id: request.from_account_id,
|
|
account_id: request.account_id,
|
|
new_state: old_state.clone(),
|
|
old_state,
|
|
created: VecMap::with_capacity(request.create.len()),
|
|
not_created: VecMap::new(),
|
|
};
|
|
|
|
let from_cache = self
|
|
.get_cached_messages(from_account_id)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
let from_message_ids = if access_token.is_member(from_account_id) {
|
|
from_cache.email_document_ids()
|
|
} else {
|
|
from_cache.shared_messages(access_token, Acl::ReadItems)
|
|
};
|
|
|
|
let can_add_mailbox_ids = if access_token.is_shared(account_id) {
|
|
cache.shared_mailboxes(access_token, Acl::AddItems).into()
|
|
} else {
|
|
None
|
|
};
|
|
let on_success_delete = request.on_success_destroy_original.unwrap_or(false);
|
|
let mut destroy_ids = Vec::new();
|
|
|
|
'create: for (id, create) in request.create.into_valid() {
|
|
let mut from_message_id = None;
|
|
let mut mailboxes = Vec::new();
|
|
let mut keywords = Vec::new();
|
|
let mut received_at = None;
|
|
|
|
for (property, value) in create.into_expanded_object() {
|
|
match (property, value) {
|
|
(Key::Property(EmailProperty::Id), Value::Element(EmailValue::Id(src))) => {
|
|
from_message_id = Some(src);
|
|
}
|
|
(Key::Property(EmailProperty::MailboxIds), Value::Object(ids)) => {
|
|
mailboxes = ids
|
|
.into_expanded_boolean_set()
|
|
.filter_map(|id| {
|
|
id.try_into_property()?.try_into_id()?.document_id().into()
|
|
})
|
|
.collect();
|
|
}
|
|
(Key::Property(EmailProperty::Keywords), Value::Object(keywords_)) => {
|
|
keywords = keywords_
|
|
.into_expanded_boolean_set()
|
|
.filter_map(|id| id.try_into_property()?.try_into_keyword())
|
|
.collect();
|
|
}
|
|
(Key::Property(EmailProperty::Pointer(pointer)), value) => {
|
|
match handle_email_patch(&pointer, value) {
|
|
PatchResult::SetKeyword(keyword) => {
|
|
if !keywords.contains(keyword) {
|
|
keywords.push(keyword.clone());
|
|
}
|
|
}
|
|
PatchResult::RemoveKeyword(keyword) => {
|
|
keywords.retain(|k| k != keyword);
|
|
}
|
|
PatchResult::AddMailbox(id) => {
|
|
if !mailboxes.contains(&id) {
|
|
mailboxes.push(id);
|
|
}
|
|
}
|
|
PatchResult::RemoveMailbox(id) => {
|
|
mailboxes.retain(|mid| mid != &id);
|
|
}
|
|
PatchResult::Invalid(set_error) => {
|
|
response.not_created.append(id, set_error);
|
|
continue 'create;
|
|
}
|
|
}
|
|
}
|
|
(
|
|
Key::Property(EmailProperty::ReceivedAt),
|
|
Value::Element(EmailValue::Date(value)),
|
|
) => {
|
|
received_at = value.into();
|
|
}
|
|
(property, _) => {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(property.into_owned())
|
|
.with_description("Invalid property or value.".to_string()),
|
|
);
|
|
continue 'create;
|
|
}
|
|
}
|
|
}
|
|
|
|
let Some(from_message_id) = from_message_id else {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(EmailProperty::Id)
|
|
.with_description("Missing or invalid \"id\" property."),
|
|
);
|
|
continue 'create;
|
|
};
|
|
if !from_message_ids.contains(from_message_id.document_id()) {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::not_found().with_description(format!(
|
|
"Item {} not found in account {}.",
|
|
id, response.from_account_id
|
|
)),
|
|
);
|
|
continue 'create;
|
|
}
|
|
|
|
// Make sure message belongs to at least one mailbox
|
|
if mailboxes.is_empty() {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(EmailProperty::MailboxIds)
|
|
.with_description("Message has to belong to at least one mailbox."),
|
|
);
|
|
continue 'create;
|
|
}
|
|
|
|
// Verify that the mailboxIds are valid
|
|
for mailbox_id in &mailboxes {
|
|
if !cache.has_mailbox_id(mailbox_id) {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(EmailProperty::MailboxIds)
|
|
.with_description(format!("mailboxId {mailbox_id} does not exist.")),
|
|
);
|
|
continue 'create;
|
|
} else if matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(*mailbox_id)) {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::forbidden().with_description(format!(
|
|
"You are not allowed to add messages to mailbox {mailbox_id}."
|
|
)),
|
|
);
|
|
continue 'create;
|
|
}
|
|
}
|
|
|
|
// Add response
|
|
match self
|
|
.copy_message(
|
|
from_account_id,
|
|
from_message_id.document_id(),
|
|
account_id,
|
|
mailboxes,
|
|
keywords,
|
|
received_at.map(|dt| dt.timestamp() as u64),
|
|
session.session_id,
|
|
)
|
|
.await?
|
|
{
|
|
Ok(email) => {
|
|
response
|
|
.created
|
|
.append(id, ingested_into_object(email).into());
|
|
}
|
|
Err(err) => {
|
|
response.not_created.append(
|
|
id,
|
|
match err {
|
|
CopyMessageError::NotFound => SetError::not_found()
|
|
.with_description("Message not found in account."),
|
|
CopyMessageError::OverQuota => SetError::over_quota(),
|
|
CopyMessageError::AlreadyExists(existing) => SetError::already_exists()
|
|
.with_existing_id(types::id::Id::from(existing)),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add to destroy list
|
|
if on_success_delete {
|
|
destroy_ids.push(MaybeInvalid::Value(from_message_id));
|
|
}
|
|
}
|
|
|
|
// Update state
|
|
if !response.created.is_empty() {
|
|
response.new_state = self.get_cached_messages(account_id).await?.get_state(false);
|
|
}
|
|
|
|
// Destroy ids
|
|
if on_success_delete && !destroy_ids.is_empty() {
|
|
*next_call = Call {
|
|
id: String::new(),
|
|
name: MethodName::new(MethodObject::Email, MethodFunction::Set),
|
|
method: RequestMethod::Set(SetRequestMethod::Email(Box::new(SetRequest {
|
|
account_id: request.from_account_id,
|
|
if_in_state: request.destroy_from_if_in_state,
|
|
create: None,
|
|
update: None,
|
|
destroy: MaybeResultReference::Value(destroy_ids).into(),
|
|
arguments: Default::default(),
|
|
}))),
|
|
}
|
|
.into();
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
}
|