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,136 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::StateManager;
|
||||
use common::Server;
|
||||
use email::sieve::{SieveScript, ingest::SieveScriptIngest};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::sieve::{Sieve, SieveProperty, SieveValue},
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
blob::{BlobClass, BlobId, BlobSection},
|
||||
collection::{Collection, SyncCollection},
|
||||
field::SieveField,
|
||||
};
|
||||
|
||||
pub trait SieveScriptGet: Sync + Send {
|
||||
fn sieve_script_get(
|
||||
&self,
|
||||
request: GetRequest<Sieve>,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<Sieve>>> + Send;
|
||||
}
|
||||
|
||||
impl SieveScriptGet for Server {
|
||||
async fn sieve_script_get(
|
||||
&self,
|
||||
mut request: GetRequest<Sieve>,
|
||||
) -> trc::Result<GetResponse<Sieve>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
SieveProperty::Id,
|
||||
SieveProperty::Name,
|
||||
SieveProperty::BlobId,
|
||||
SieveProperty::IsActive,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let script_ids = self
|
||||
.document_ids(account_id, Collection::SieveScript, SieveField::Name)
|
||||
.await?;
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
script_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: self
|
||||
.get_state(account_id, SyncCollection::SieveScript)
|
||||
.await?
|
||||
.into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
let active_script_id = self.sieve_script_get_active_id(account_id).await?;
|
||||
|
||||
for id in ids {
|
||||
// Obtain the sieve script object
|
||||
let document_id = id.document_id();
|
||||
if !script_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
}
|
||||
let sieve_ = if let Some(sieve) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::SieveScript,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
sieve
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let sieve = sieve_
|
||||
.unarchive::<SieveScript>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
match property {
|
||||
SieveProperty::Id => {
|
||||
result.insert_unchecked(SieveProperty::Id, id);
|
||||
}
|
||||
SieveProperty::Name => {
|
||||
result.insert_unchecked(SieveProperty::Name, &sieve.name);
|
||||
}
|
||||
SieveProperty::IsActive => {
|
||||
result.insert_unchecked(
|
||||
SieveProperty::IsActive,
|
||||
active_script_id == Some(document_id),
|
||||
);
|
||||
}
|
||||
SieveProperty::BlobId => {
|
||||
let blob_id = BlobId {
|
||||
hash: (&sieve.blob_hash).into(),
|
||||
class: BlobClass::Linked {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
document_id,
|
||||
},
|
||||
section: BlobSection {
|
||||
size: u32::from(sieve.size) as usize,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
|
||||
result.insert_unchecked(
|
||||
SieveProperty::BlobId,
|
||||
Value::Element(SieveValue::BlobId(blob_id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
pub mod validate;
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
|
||||
use common::Server;
|
||||
use email::sieve::ingest::SieveScriptIngest;
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::sieve::{Sieve, SieveComparator, SieveFilter},
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
IndexKeyPrefix, IterateParams, U32_LEN,
|
||||
roaring::RoaringBitmap,
|
||||
search::{SearchFilter, SearchQuery},
|
||||
write::{SearchIndex, key::DeserializeBigEndian},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::SieveField,
|
||||
};
|
||||
|
||||
pub trait SieveScriptQuery: Sync + Send {
|
||||
fn sieve_script_query(
|
||||
&self,
|
||||
request: QueryRequest<Sieve>,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
impl SieveScriptQuery for Server {
|
||||
async fn sieve_script_query(
|
||||
&self,
|
||||
mut request: QueryRequest<Sieve>,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
let active_script_id = if request
|
||||
.filter
|
||||
.iter()
|
||||
.any(|f| matches!(f, Filter::Property(SieveFilter::IsActive(_))))
|
||||
|| request.sort.as_ref().is_some_and(|s| {
|
||||
s.iter()
|
||||
.any(|c| matches!(c.property, SieveComparator::IsActive))
|
||||
}) {
|
||||
self.sieve_script_get_active_id(account_id).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut document_ids = RoaringBitmap::new();
|
||||
let mut names = Vec::new();
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
IndexKeyPrefix {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
field: SieveField::Name.into(),
|
||||
},
|
||||
IndexKeyPrefix {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
field: u8::from(SieveField::Name) + 1,
|
||||
},
|
||||
)
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
|
||||
|
||||
names.push((
|
||||
document_id,
|
||||
key.get(IndexKeyPrefix::len()..key.len() - U32_LEN)
|
||||
.and_then(|v| std::str::from_utf8(v).ok())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
));
|
||||
|
||||
document_ids.insert(document_id);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
SieveFilter::Name(name) => {
|
||||
let name = name.to_lowercase();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
names
|
||||
.iter()
|
||||
.filter_map(|(id, n)| (n.contains(&name)).then_some(*id))
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
}
|
||||
SieveFilter::IsActive(is_active) => {
|
||||
if is_active {
|
||||
if let Some(active_script_id) = active_script_id {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter([
|
||||
active_script_id,
|
||||
])));
|
||||
} else {
|
||||
// No active script, so no results
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::new()));
|
||||
}
|
||||
} else {
|
||||
let mut inactive_set = document_ids.clone();
|
||||
if let Some(active_script_id) = active_script_id {
|
||||
inactive_set.remove(active_script_id);
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(inactive_set));
|
||||
}
|
||||
}
|
||||
SieveFilter::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
|
||||
}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse sort criteria
|
||||
let mut sort_by_active = None;
|
||||
for comparator in request
|
||||
.sort
|
||||
.take()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
match comparator.property {
|
||||
SieveComparator::Name => {
|
||||
if !comparator.is_ascending {
|
||||
names.reverse();
|
||||
}
|
||||
}
|
||||
SieveComparator::IsActive => {
|
||||
sort_by_active = Some(comparator.is_ascending);
|
||||
}
|
||||
SieveComparator::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut results = SearchQuery::new(SearchIndex::InMemory)
|
||||
.with_filters(filters)
|
||||
.with_mask(document_ids)
|
||||
.filter()
|
||||
.into_bitmap();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
self.get_state(account_id, SyncCollection::SieveScript)
|
||||
.await?,
|
||||
&request,
|
||||
);
|
||||
|
||||
if !results.is_empty() {
|
||||
if matches!(sort_by_active, Some(true))
|
||||
&& results.remove(active_script_id.unwrap_or_default())
|
||||
&& !response.add(0, active_script_id.unwrap())
|
||||
{
|
||||
return response.build();
|
||||
}
|
||||
|
||||
let mut last_id = None;
|
||||
for (document_id, _) in names {
|
||||
if results.contains(document_id) {
|
||||
if sort_by_active.is_some() && Some(document_id) == active_script_id {
|
||||
last_id = Some(document_id);
|
||||
} else if !response.add(0, document_id) {
|
||||
return response.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_id) = last_id {
|
||||
response.add(0, active_id);
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{blob::download::BlobDownload, changes::state::StateManager};
|
||||
use common::{
|
||||
Server,
|
||||
auth::{AccessToken, AccountCache},
|
||||
storage::index::ObjectIndexBuilder,
|
||||
};
|
||||
use email::sieve::{
|
||||
ArchivedSieveScript, SieveScript, delete::SieveScriptDelete, ingest::SieveScriptIngest,
|
||||
};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::sieve::{Sieve, SieveProperty, SieveValue},
|
||||
references::resolve::ResolveCreatedReference,
|
||||
request::{MaybeInvalid, reference::MaybeIdReference},
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use rand::distr::Alphanumeric;
|
||||
use registry::schema::enums::StorageQuota;
|
||||
use sieve::compiler::ErrorType;
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
Serialize, SerializeInfallible, ValueKey,
|
||||
rand::{RngExt, rng},
|
||||
write::{AlignedBytes, Archive, Archiver, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
blob::{BlobClass, BlobId, BlobSection},
|
||||
collection::{Collection, SyncCollection},
|
||||
field::{PrincipalField, SieveField},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub struct SetContext<'x> {
|
||||
account_id: u32,
|
||||
access_token: &'x AccessToken,
|
||||
account_cache: &'x AccountCache,
|
||||
response: SetResponse<Sieve>,
|
||||
}
|
||||
|
||||
pub trait SieveScriptSet: Sync + Send {
|
||||
fn sieve_script_set(
|
||||
&self,
|
||||
request: SetRequest<'_, Sieve>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<Sieve>>> + Send;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn sieve_set_item<'x>(
|
||||
&self,
|
||||
changes_: Value<'_, SieveProperty, SieveValue>,
|
||||
update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
|
||||
ctx: &SetContext,
|
||||
session_id: u64,
|
||||
) -> impl Future<Output = trc::Result<Result<SetItemResponse<'x>, SetError<SieveProperty>>>> + Send;
|
||||
}
|
||||
|
||||
impl SieveScriptSet for Server {
|
||||
async fn sieve_script_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, Sieve>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<Sieve>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let sieve_ids = self
|
||||
.document_ids(account_id, Collection::SieveScript, SieveField::Name)
|
||||
.await?;
|
||||
let account = self.account(account_id).await.caused_by(trc::location!())?;
|
||||
let mut ctx = SetContext {
|
||||
account_id,
|
||||
access_token,
|
||||
account_cache: &account,
|
||||
response: SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
|
||||
.with_state(
|
||||
self.assert_state(
|
||||
account_id,
|
||||
SyncCollection::SieveScript,
|
||||
&request.if_in_state,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
};
|
||||
let will_destroy = ctx.response.collect_will_destroy(request.unwrap_destroy());
|
||||
|
||||
// Validate active script id
|
||||
if let Some(MaybeIdReference::Id(id)) = &request.arguments.on_success_activate_script
|
||||
&& !sieve_ids.contains(id.document_id())
|
||||
{
|
||||
request.arguments.on_success_activate_script = None;
|
||||
}
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut activations = Vec::new();
|
||||
for (id, object) in request.unwrap_create() {
|
||||
if sieve_ids.len()
|
||||
< self.object_quota(account.object_quotas(), StorageQuota::MaxSieveScripts) as u64
|
||||
{
|
||||
match self
|
||||
.sieve_set_item(object, None, &ctx, session.session_id)
|
||||
.await?
|
||||
{
|
||||
Ok(mut result) => {
|
||||
// Store blob
|
||||
let sieve = &mut result.builder.changes_mut().unwrap();
|
||||
let (blob_hash, blob_hold) = self
|
||||
.put_temporary_blob(
|
||||
account_id,
|
||||
result.blob_update.as_ref().unwrap(),
|
||||
60,
|
||||
)
|
||||
.await?;
|
||||
sieve.blob_hash = blob_hash;
|
||||
let blob_size = sieve.size as usize;
|
||||
let blob_hash = sieve.blob_hash.clone();
|
||||
|
||||
// Write record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::SieveScript, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::SieveScript)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
result
|
||||
.builder
|
||||
.with_changed_by(ctx.access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.clear(blob_hold)
|
||||
.commit_point();
|
||||
|
||||
// Set isActive if needed
|
||||
if let Some(set_item) = result.set_item {
|
||||
activations.push((document_id, set_item));
|
||||
}
|
||||
|
||||
let mut result = Map::with_capacity(1)
|
||||
.with_key_value(SieveProperty::Id, SieveValue::Id(document_id.into()))
|
||||
.with_key_value(
|
||||
SieveProperty::BlobId,
|
||||
SieveValue::BlobId(BlobId {
|
||||
hash: blob_hash,
|
||||
class: BlobClass::Linked {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
document_id,
|
||||
},
|
||||
section: BlobSection {
|
||||
size: blob_size,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Update active script if needed
|
||||
if let Some(MaybeIdReference::Reference(id_ref)) =
|
||||
&request.arguments.on_success_activate_script
|
||||
&& id_ref == &id
|
||||
{
|
||||
request.arguments.on_success_activate_script =
|
||||
Some(MaybeIdReference::Id(Id::from(document_id)));
|
||||
result.insert_unchecked(SieveProperty::IsActive, true);
|
||||
}
|
||||
|
||||
// Add result with updated blobId
|
||||
ctx.response.created.insert(id, result.into());
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.response.not_created.append(id, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.response.not_created.append(
|
||||
id,
|
||||
SetError::new(SetErrorType::OverQuota).with_description(concat!(
|
||||
"There are too many sieve scripts, ",
|
||||
"please delete some before adding a new one."
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
ctx.response
|
||||
.not_updated
|
||||
.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
ctx.response
|
||||
.not_updated
|
||||
.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain sieve script
|
||||
let document_id = id.document_id();
|
||||
if let Some(sieve_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::SieveScript,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
let sieve = sieve_
|
||||
.to_unarchived::<SieveScript>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
match self
|
||||
.sieve_set_item(
|
||||
object,
|
||||
(document_id, sieve).into(),
|
||||
&ctx,
|
||||
session.session_id,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(mut result) => {
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::SieveScript)
|
||||
.with_document(document_id);
|
||||
|
||||
let blob_id = if let Some(blob) = result.blob_update.take() {
|
||||
// Store blob
|
||||
let sieve = &mut result.builder.changes_mut().unwrap();
|
||||
let (blob_hash, blob_hold) =
|
||||
self.put_temporary_blob(account_id, &blob, 60).await?;
|
||||
sieve.blob_hash = blob_hash;
|
||||
batch.clear(blob_hold);
|
||||
|
||||
BlobId {
|
||||
hash: sieve.blob_hash.clone(),
|
||||
class: BlobClass::Linked {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
document_id,
|
||||
},
|
||||
section: BlobSection {
|
||||
size: sieve.size as usize,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set isActive if needed
|
||||
if let Some(set_item) = result.set_item {
|
||||
activations.push((document_id, set_item));
|
||||
}
|
||||
|
||||
// Write record
|
||||
batch
|
||||
.custom(
|
||||
result
|
||||
.builder
|
||||
.with_changed_by(ctx.access_token.account_tenant_ids()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
|
||||
// Update blobId property if needed
|
||||
let mut result = Map::with_capacity(1);
|
||||
if let Some(blob_id) = blob_id {
|
||||
result.insert_unchecked(
|
||||
SieveProperty::BlobId,
|
||||
SieveValue::BlobId(blob_id),
|
||||
);
|
||||
}
|
||||
|
||||
// Add active script property if needed
|
||||
if let Some(MaybeIdReference::Id(id)) =
|
||||
&request.arguments.on_success_activate_script
|
||||
&& document_id == id.document_id()
|
||||
{
|
||||
result.insert_unchecked(SieveProperty::IsActive, true);
|
||||
}
|
||||
|
||||
// Add result
|
||||
ctx.response.updated.append(
|
||||
id,
|
||||
if !result.is_empty() {
|
||||
Value::Object(result).into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.response.not_updated.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
let active_script_id = self.sieve_script_get_active_id(account_id).await?;
|
||||
for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
if sieve_ids.contains(document_id) {
|
||||
if active_script_id != Some(document_id) {
|
||||
if self
|
||||
.sieve_script_delete(account_id, document_id, ctx.access_token, &mut batch)
|
||||
.await?
|
||||
{
|
||||
ctx.response.destroyed.push(id);
|
||||
} else {
|
||||
ctx.response.not_destroyed.append(id, SetError::not_found());
|
||||
}
|
||||
} else {
|
||||
ctx.response.not_destroyed.append(
|
||||
id,
|
||||
SetError::new(SetErrorType::ScriptIsActive)
|
||||
.with_description("Deactivate Sieve script before deletion."),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ctx.response.not_destroyed.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
|
||||
// Non-standard script activation handling
|
||||
let mut on_success_deactivate_script = request
|
||||
.arguments
|
||||
.on_success_deactivate_script
|
||||
.unwrap_or(false);
|
||||
if activations.len() == 1 {
|
||||
let (document_id, set_item) = activations[0];
|
||||
let is_active = active_script_id.is_some_and(|active_id| active_id == document_id);
|
||||
if set_item {
|
||||
if request.arguments.on_success_activate_script.is_none() && !is_active {
|
||||
request.arguments.on_success_activate_script =
|
||||
Some(MaybeIdReference::Id(document_id.into()));
|
||||
}
|
||||
} else if !on_success_deactivate_script && is_active {
|
||||
on_success_deactivate_script = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Activate / deactivate scripts
|
||||
if ctx.response.not_created.is_empty()
|
||||
&& ctx.response.not_updated.is_empty()
|
||||
&& ctx.response.not_destroyed.is_empty()
|
||||
&& (request.arguments.on_success_activate_script.is_some()
|
||||
|| on_success_deactivate_script)
|
||||
{
|
||||
if let Some(MaybeIdReference::Id(id)) = request.arguments.on_success_activate_script {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.set(PrincipalField::ActiveScriptId, id.document_id().serialize());
|
||||
} else if on_success_deactivate_script {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::ActiveScriptId);
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty()
|
||||
&& let Ok(change_id) = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.last_change_id(account_id)
|
||||
{
|
||||
ctx.response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(ctx.response)
|
||||
}
|
||||
|
||||
#[allow(clippy::blocks_in_conditions)]
|
||||
async fn sieve_set_item<'x>(
|
||||
&self,
|
||||
changes_: Value<'_, SieveProperty, SieveValue>,
|
||||
update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
|
||||
ctx: &SetContext<'_>,
|
||||
session_id: u64,
|
||||
) -> trc::Result<Result<SetItemResponse<'x>, SetError<SieveProperty>>> {
|
||||
// Vacation script cannot be modified
|
||||
if update
|
||||
.as_ref()
|
||||
.is_some_and(|(_, obj)| obj.inner.name.eq_ignore_ascii_case("vacation"))
|
||||
{
|
||||
return Ok(Err(SetError::forbidden().with_description(concat!(
|
||||
"The 'vacation' script cannot be modified, ",
|
||||
"use VacationResponse/set instead."
|
||||
))));
|
||||
}
|
||||
|
||||
// Parse properties
|
||||
let mut set_item = None;
|
||||
let mut changes = update
|
||||
.as_ref()
|
||||
.map(|(_, obj)| obj.deserialize().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
let mut blob_id = None;
|
||||
for (property, mut value) in changes_.into_expanded_object() {
|
||||
if let Err(err) = ctx.response.resolve_self_references(&mut value, 0, false) {
|
||||
return Ok(Err(err));
|
||||
};
|
||||
match (&property, value) {
|
||||
(Key::Property(SieveProperty::Name), Value::Str(value)) => {
|
||||
if value.len() > self.core.email.sieve_max_script_name {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(property.into_owned())
|
||||
.with_description("Script name is too long.")));
|
||||
} else if value.eq_ignore_ascii_case("vacation") {
|
||||
return Ok(Err(SetError::forbidden()
|
||||
.with_property(property.into_owned())
|
||||
.with_description(
|
||||
"The 'vacation' name is reserved, please use a different name.",
|
||||
)));
|
||||
} else if update
|
||||
.as_ref()
|
||||
.is_none_or(|(_, obj)| obj.inner.name != value.as_ref())
|
||||
&& let Some(id) = self
|
||||
.document_ids_matching(
|
||||
ctx.account_id,
|
||||
Collection::SieveScript,
|
||||
SieveField::Name,
|
||||
value.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
.min()
|
||||
{
|
||||
return Ok(Err(SetError::already_exists()
|
||||
.with_existing_id(id.into())
|
||||
.with_description(format!(
|
||||
"A sieve script with name '{}' already exists.",
|
||||
value
|
||||
))));
|
||||
}
|
||||
|
||||
changes.name = value.into_owned();
|
||||
}
|
||||
(
|
||||
Key::Property(SieveProperty::BlobId),
|
||||
Value::Element(SieveValue::BlobId(value)),
|
||||
) => {
|
||||
blob_id = value.into();
|
||||
continue;
|
||||
}
|
||||
(Key::Property(SieveProperty::Name), Value::Null) => {
|
||||
continue;
|
||||
}
|
||||
(Key::Property(SieveProperty::IsActive), Value::Bool(value)) => {
|
||||
set_item = Some(value);
|
||||
continue;
|
||||
}
|
||||
(Key::Property(SieveProperty::Id), value) => {
|
||||
if update
|
||||
.as_ref()
|
||||
.map(|(document_id, _)| Id::from(*document_id))
|
||||
.is_none_or(|expected| !crate::matches_id(&value, expected))
|
||||
{
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(SieveProperty::Id)
|
||||
.with_description("The id property is immutable.".to_string())));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(property.into_owned())
|
||||
.with_description("Invalid property or value.".to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update.is_none() {
|
||||
// Add name if missing
|
||||
if changes.name.is_empty() {
|
||||
changes.name = rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(15)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
}
|
||||
}
|
||||
|
||||
let blob_update = if let Some(blob_id) = blob_id {
|
||||
if update.as_ref().is_none_or( |(document_id, _)| {
|
||||
!matches!(blob_id.class, BlobClass::Linked { account_id, collection, document_id: d } if account_id == ctx.account_id && collection == u8::from(Collection::SieveScript) && *document_id == d)
|
||||
}) {
|
||||
// Check access
|
||||
if let Some(mut bytes) = self.blob_download(&blob_id, ctx.access_token).await? {
|
||||
// Check quota
|
||||
match self
|
||||
.has_available_quota(ctx.account_cache, bytes.len() as u64)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota))
|
||||
|| err.matches(trc::EventType::Limit(trc::LimitEvent::TenantQuota))
|
||||
{
|
||||
trc::error!(err.account_id(ctx.account_id).span_id(session_id));
|
||||
return Ok(Err(SetError::over_quota()));
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile script
|
||||
match self.core.sieve.untrusted_compiler.compile(&bytes) {
|
||||
Ok(script) => {
|
||||
changes.size = bytes.len() as u32;
|
||||
bytes.extend(Archiver::new(script).untrusted().serialize().caused_by(trc::location!())?);
|
||||
bytes.into()
|
||||
}
|
||||
Err(err) => {
|
||||
return Ok(Err(SetError::new(
|
||||
if let ErrorType::ScriptTooLong = &err.error_type() {
|
||||
SetErrorType::TooLarge
|
||||
} else {
|
||||
SetErrorType::InvalidScript
|
||||
},
|
||||
)
|
||||
.with_description(err.to_string())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(Err(SetError::new(SetErrorType::BlobNotFound)
|
||||
.with_property(SieveProperty::BlobId)
|
||||
.with_description("Blob does not exist.")));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if update.is_none() {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(SieveProperty::BlobId)
|
||||
.with_description("Missing blobId.")));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Validate
|
||||
Ok(Ok(SetItemResponse {
|
||||
builder: ObjectIndexBuilder::new()
|
||||
.with_changes(changes)
|
||||
.with_current_opt(update.map(|(_, current)| current)),
|
||||
blob_update,
|
||||
set_item,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetItemResponse<'x> {
|
||||
builder: ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
|
||||
blob_update: Option<Vec<u8>>,
|
||||
set_item: Option<bool>,
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::blob::download::BlobDownload;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use std::future::Future;
|
||||
|
||||
pub trait SieveScriptValidate: Sync + Send {
|
||||
fn sieve_script_validate(
|
||||
&self,
|
||||
request: ValidateSieveScriptRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<ValidateSieveScriptResponse>> + Send;
|
||||
}
|
||||
|
||||
impl SieveScriptValidate for Server {
|
||||
async fn sieve_script_validate(
|
||||
&self,
|
||||
request: ValidateSieveScriptRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<ValidateSieveScriptResponse> {
|
||||
Ok(ValidateSieveScriptResponse {
|
||||
account_id: request.account_id,
|
||||
error: match request.blob_id {
|
||||
MaybeInvalid::Value(blob_id) => {
|
||||
match self
|
||||
.blob_download(&blob_id, access_token)
|
||||
.await?
|
||||
.map(|bytes| self.core.sieve.untrusted_compiler.compile(&bytes))
|
||||
{
|
||||
Some(Ok(_)) => None,
|
||||
Some(Err(err)) => SetError::new(SetErrorType::InvalidScript)
|
||||
.with_description(err.to_string())
|
||||
.into(),
|
||||
None => SetError::new(SetErrorType::BlobNotFound).into(),
|
||||
}
|
||||
}
|
||||
MaybeInvalid::Invalid(_) => SetError::new(SetErrorType::BlobNotFound).into(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user