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,272 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user