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,421 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
api::query::QueryResponseBuilder,
|
||||
registry::{
|
||||
mapping::{RegistryGetResponse, RegistryQueryResponse, RegistrySetResponse},
|
||||
query::RegistryQueryFilters,
|
||||
},
|
||||
};
|
||||
use jmap_proto::{error::set::SetError, types::state::State};
|
||||
use jmap_tools::{Key, Value};
|
||||
use registry::{
|
||||
jmap::IntoValue,
|
||||
schema::prelude::{Object, ObjectInner, ObjectType, Property},
|
||||
types::{EnumImpl, datetime::UTCDateTime},
|
||||
};
|
||||
use smtp::reporting::index::{ExternalReportIndex, InternalReportIndex};
|
||||
use std::str::FromStr;
|
||||
use store::{
|
||||
U64_LEN, ValueKey,
|
||||
registry::{RegistryFilter, RegistryFilterValue, RegistryQuery},
|
||||
write::{BatchBuilder, RegistryClass, ValueClass, key::KeySerializer},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
|
||||
pub(crate) async fn report_set(
|
||||
mut set: RegistrySetResponse<'_>,
|
||||
) -> trc::Result<RegistrySetResponse<'_>> {
|
||||
let object_id = set.object_type.to_id();
|
||||
|
||||
// Reports cannot be created
|
||||
set.fail_all_create("Reports cannot be created");
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
if matches!(
|
||||
set.object_type,
|
||||
ObjectType::DmarcInternalReport | ObjectType::TlsInternalReport
|
||||
) {
|
||||
let now = UTCDateTime::now();
|
||||
'outer: for (id, value) in set.update.drain(..) {
|
||||
// Extract new deliverAt value
|
||||
let mut deliver_at = None;
|
||||
for (key, value) in value.into_expanded_object() {
|
||||
match (key, value) {
|
||||
(Key::Property(Property::DeliverAt), Value::Str(deliver_at_)) => {
|
||||
deliver_at = UTCDateTime::from_str(deliver_at_.as_ref())
|
||||
.ok()
|
||||
.filter(|da| *da > now);
|
||||
if deliver_at.is_none() {
|
||||
set.response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_patch()
|
||||
.with_property(Property::DeliverAt)
|
||||
.with_description("Invalid value for property"),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
(Key::Property(Property::Id), _) => {}
|
||||
(key, _) => {
|
||||
set.response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties().with_property(key.into_owned()),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(deliver_at) = deliver_at else {
|
||||
set.response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_patch()
|
||||
.with_property(Key::Property(Property::DeliverAt))
|
||||
.with_description("Missing required property"),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let item_id = id.id();
|
||||
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
|
||||
if let Some(mut report_obj) = set
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Object>(ValueKey::from(key.clone()))
|
||||
.await?
|
||||
{
|
||||
match &mut report_obj.inner {
|
||||
ObjectInner::DmarcInternalReport(report) => {
|
||||
report.reschedule_ops(&mut batch, item_id, report_obj.revision, deliver_at);
|
||||
}
|
||||
ObjectInner::TlsInternalReport(report) => {
|
||||
report.reschedule_ops(&mut batch, item_id, report_obj.revision, deliver_at);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
batch.commit_point();
|
||||
|
||||
set.response.updated.append(id, None);
|
||||
} else {
|
||||
set.response.not_updated.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// External reports cannot be updated
|
||||
set.fail_all_update("External reports cannot be updated");
|
||||
}
|
||||
|
||||
// Process reports to destroy
|
||||
let tenant_id = set.access_token.tenant_id().map(Id::from);
|
||||
for id in set.destroy.drain(..) {
|
||||
let item_id = id.id();
|
||||
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
|
||||
if let Some(report) = set
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Object>(ValueKey::from(key))
|
||||
.await?
|
||||
.filter(|report| {
|
||||
!set.is_tenant_filtered || report.inner.member_tenant_id() == tenant_id
|
||||
})
|
||||
{
|
||||
match &report.inner {
|
||||
ObjectInner::DmarcExternalReport(report) => {
|
||||
report.write_ops(&mut batch, item_id, false);
|
||||
}
|
||||
ObjectInner::TlsExternalReport(report) => {
|
||||
report.write_ops(&mut batch, item_id, false);
|
||||
}
|
||||
ObjectInner::ArfExternalReport(report) => {
|
||||
report.write_ops(&mut batch, item_id, false);
|
||||
}
|
||||
ObjectInner::DmarcInternalReport(report) => {
|
||||
report.write_ops(&mut batch, item_id, false);
|
||||
}
|
||||
ObjectInner::TlsInternalReport(report) => {
|
||||
report.write_ops(&mut batch, item_id, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
batch.commit_point();
|
||||
|
||||
set.response.destroyed.push(id);
|
||||
} else {
|
||||
set.response.not_destroyed.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
|
||||
if !batch.is_empty() {
|
||||
set.server
|
||||
.store()
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
pub(crate) async fn report_get(
|
||||
mut get: RegistryGetResponse<'_>,
|
||||
) -> trc::Result<RegistryGetResponse<'_>> {
|
||||
let object_id = get.object_type.to_id();
|
||||
let ids = if let Some(ids) = get.ids.take() {
|
||||
ids
|
||||
} else if matches!(
|
||||
get.object_type,
|
||||
ObjectType::DmarcExternalReport
|
||||
| ObjectType::TlsExternalReport
|
||||
| ObjectType::ArfExternalReport
|
||||
) {
|
||||
if get.is_tenant_filtered {
|
||||
get.server.registry().query::<Vec<Id>>(
|
||||
RegistryQuery::new(get.object_type)
|
||||
.with_tenant(get.access_token.tenant_id())
|
||||
.with_limit(get.server.core.jmap.get_max_objects),
|
||||
)
|
||||
} else {
|
||||
get.server.registry().query::<Vec<Id>>(
|
||||
RegistryQuery::new(get.object_type)
|
||||
.greater_than(Property::ExpiresAt, 0u64)
|
||||
.with_limit(get.server.core.jmap.get_max_objects),
|
||||
)
|
||||
}
|
||||
.await?
|
||||
} else {
|
||||
get.server
|
||||
.registry()
|
||||
.query::<Vec<Id>>(
|
||||
RegistryQuery::new(get.object_type)
|
||||
.filter(RegistryFilter::greater_than(
|
||||
Property::Domain,
|
||||
RegistryFilterValue::Bytes(vec![]),
|
||||
true,
|
||||
))
|
||||
.with_limit(get.server.core.jmap.get_max_objects),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let tenant_id = get.access_token.tenant_id().map(Id::from);
|
||||
for id in ids {
|
||||
if let Some(report) = get
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Object>(ValueKey::from(ValueClass::Registry(RegistryClass::Item {
|
||||
object_id,
|
||||
item_id: id.id(),
|
||||
})))
|
||||
.await?
|
||||
.filter(|report| {
|
||||
!get.is_tenant_filtered || report.inner.member_tenant_id() == tenant_id
|
||||
})
|
||||
{
|
||||
get.insert(id, report.into_value());
|
||||
} else {
|
||||
get.not_found(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(get)
|
||||
}
|
||||
|
||||
pub(crate) async fn report_query(
|
||||
mut req: RegistryQueryResponse<'_>,
|
||||
) -> trc::Result<QueryResponseBuilder> {
|
||||
let mut query = store::registry::RegistryQuery::new(req.object_type)
|
||||
.with_tenant(req.access_token.tenant_id());
|
||||
let is_internal = matches!(
|
||||
req.object_type,
|
||||
ObjectType::DmarcInternalReport | ObjectType::TlsInternalReport
|
||||
);
|
||||
|
||||
req.request
|
||||
.extract_filters(|property, op, value| match property {
|
||||
Property::Domain => {
|
||||
if let serde_json::Value::String(value) = value {
|
||||
match req.object_type {
|
||||
ObjectType::DmarcInternalReport => {
|
||||
query.filters.push(RegistryFilter::greater_than_or_equal(
|
||||
property,
|
||||
RegistryFilterValue::Bytes(
|
||||
KeySerializer::new(value.len() + U64_LEN)
|
||||
.write(value.as_str())
|
||||
.write(0u64)
|
||||
.finalize(),
|
||||
),
|
||||
true,
|
||||
));
|
||||
query.filters.push(RegistryFilter::less_than_or_equal(
|
||||
property,
|
||||
RegistryFilterValue::Bytes(
|
||||
KeySerializer::new(value.len() + U64_LEN)
|
||||
.write(value.as_str())
|
||||
.write(u64::MAX)
|
||||
.finalize(),
|
||||
),
|
||||
true,
|
||||
));
|
||||
|
||||
true
|
||||
}
|
||||
ObjectType::TlsInternalReport => {
|
||||
query
|
||||
.filters
|
||||
.push(RegistryFilter::equal(property, value, true));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Property::Text if !is_internal => {
|
||||
if let serde_json::Value::String(value) = value {
|
||||
query.filters.push(RegistryFilter::text(property, value));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Property::MemberTenantId if !is_internal => {
|
||||
if req.access_token.tenant_id().is_none()
|
||||
&& let Some(id) = value.as_str().and_then(|s| Id::from_str(s).ok())
|
||||
{
|
||||
query
|
||||
.filters
|
||||
.push(RegistryFilter::equal(property, id.id(), false));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Property::TotalFailedSessions | Property::TotalSuccessfulSessions if !is_internal => {
|
||||
if let Some(value) = value.as_u64() {
|
||||
query.filters.push(store::registry::RegistryFilter {
|
||||
property,
|
||||
op,
|
||||
value: value.into(),
|
||||
is_pk: false,
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Property::ExpiresAt if !is_internal => {
|
||||
if let Some(value) = value
|
||||
.as_str()
|
||||
.and_then(|value| UTCDateTime::from_str(value).ok())
|
||||
{
|
||||
query.filters.push(store::registry::RegistryFilter {
|
||||
property,
|
||||
op,
|
||||
value: (value.timestamp() as u64).into(),
|
||||
is_pk: false,
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
})?;
|
||||
|
||||
let params = req
|
||||
.request
|
||||
.extract_parameters(req.server.core.jmap.query_max_results, Some(Property::Id))?;
|
||||
|
||||
if !query.has_filters() {
|
||||
if is_internal {
|
||||
query.filters.push(RegistryFilter::greater_than(
|
||||
Property::Domain,
|
||||
RegistryFilterValue::Bytes(vec![]),
|
||||
true,
|
||||
));
|
||||
} else {
|
||||
query.filters.push(RegistryFilter::greater_than(
|
||||
Property::ExpiresAt,
|
||||
0u64,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(limit) = params.limit {
|
||||
query = query.with_limit(limit);
|
||||
if let Some(anchor) = params.anchor {
|
||||
query = query.with_anchor(anchor);
|
||||
} else if let Some(position) = params.position {
|
||||
query = query.with_index_start(position);
|
||||
}
|
||||
}
|
||||
|
||||
let matches = req.server.registry().query::<Vec<Id>>(query).await?;
|
||||
let results = match params.sort_by {
|
||||
Property::Id => {
|
||||
let mut results = matches;
|
||||
if !params.sort_ascending {
|
||||
results.sort_unstable_by(|a, b| b.cmp(a));
|
||||
}
|
||||
results
|
||||
}
|
||||
Property::Domain if is_internal => {
|
||||
if !matches.is_empty() {
|
||||
req.server
|
||||
.registry()
|
||||
.sort_by_pk(
|
||||
req.object_type,
|
||||
Property::Domain,
|
||||
Some(matches),
|
||||
params.sort_ascending,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
Property::ExpiresAt if !is_internal => {
|
||||
if !matches.is_empty() {
|
||||
req.server
|
||||
.registry()
|
||||
.sort_by_index(
|
||||
req.object_type,
|
||||
Property::ExpiresAt,
|
||||
Some(matches),
|
||||
params.sort_ascending,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
property => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(format!(
|
||||
"Property {} is not supported for sorting",
|
||||
property
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Build response
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
req.server.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&req.request,
|
||||
);
|
||||
|
||||
for id in results {
|
||||
if !response.add_id(id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
Reference in New Issue
Block a user