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,427 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{calendar::Availability, calendar_event::CalendarSyntheticId};
|
||||
use calcard::{
|
||||
common::timezone::Tz,
|
||||
icalendar::{
|
||||
ArchivedICalendarClassification, ArchivedICalendarParameterValue,
|
||||
ArchivedICalendarParticipationStatus, ArchivedICalendarProperty, ArchivedICalendarStatus,
|
||||
ArchivedICalendarTransparency, ArchivedICalendarValue, ICalendarParameterName,
|
||||
},
|
||||
jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue},
|
||||
};
|
||||
use common::{
|
||||
Server, TinyCalendarPreferences,
|
||||
auth::{AccessToken, BuildAccessToken},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{CALENDAR_SUBSCRIBED, CalendarEvent, expand::RecurrenceKey},
|
||||
strip_mailto_scheme,
|
||||
};
|
||||
use jmap_proto::{
|
||||
method::availability::{
|
||||
BusyPeriod, BusyStatus, GetAvailabilityRequest, GetAvailabilityResponse,
|
||||
},
|
||||
object::calendar::IncludeInAvailability,
|
||||
request::IntoValid,
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{collections::hash_map::Entry, future::Future};
|
||||
use store::{
|
||||
ValueKey,
|
||||
ahash::AHashMap,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
TimeRange,
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
id::Id,
|
||||
};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub trait PrincipalGetAvailability: Sync + Send {
|
||||
fn principal_get_availability(
|
||||
&self,
|
||||
request: GetAvailabilityRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetAvailabilityResponse>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalGetAvailability for Server {
|
||||
async fn principal_get_availability(
|
||||
&self,
|
||||
request: GetAvailabilityRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetAvailabilityResponse> {
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::JmapPrincipalGetAvailability)
|
||||
{
|
||||
return Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("The administrator has disabled directory queries.".to_string()));
|
||||
}
|
||||
|
||||
// Process parameters
|
||||
if !request.id.is_valid() {
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("Missing principal id"));
|
||||
}
|
||||
let properties = request
|
||||
.event_properties
|
||||
.map(|props| props.into_valid().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
if properties
|
||||
.iter()
|
||||
.any(|p| !matches!(p, JSCalendarProperty::Id | JSCalendarProperty::BaseEventId))
|
||||
{
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("Only 'id' and 'baseEventId' properties are supported in results"));
|
||||
}
|
||||
let return_event_details = !properties.is_empty();
|
||||
let max_instances = self.core.groupware.max_ical_instances;
|
||||
let filter = TimeRange {
|
||||
start: request.utc_start.timestamp(),
|
||||
end: request.utc_end.timestamp(),
|
||||
};
|
||||
let principal_id = request.id.document_id();
|
||||
let principal = self
|
||||
.access_token(principal_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.build();
|
||||
let principal_account = self
|
||||
.account_info(principal_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut periods = Vec::new();
|
||||
|
||||
for account_id in principal.all_ids_by_collection(Collection::Calendar) {
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Obtain shared ids
|
||||
let is_account_owner = principal_id == account_id;
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
// Condition: The user has the "mayReadFreeBusy" permission for the calendar.
|
||||
let shared_ids = resources.shared_items(
|
||||
access_token,
|
||||
[Acl::ReadItems, Acl::SchedulingReadFreeBusy],
|
||||
true,
|
||||
);
|
||||
if shared_ids.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
shared_ids.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Condition: The event finishes after the "utcStart" argument and starts before the "utcEnd" argument.
|
||||
let mut preferences_cache: AHashMap<u32, Option<&TinyCalendarPreferences>> =
|
||||
AHashMap::default();
|
||||
'next_event: for resource in resources.resources.iter().filter(|r| {
|
||||
r.event_time_range().is_some_and(|(start, end)| {
|
||||
shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(r.document_id))
|
||||
&& filter.is_in_range(false, start, end)
|
||||
})
|
||||
}) {
|
||||
// Obtain calendar settings
|
||||
let mut include_in_availability = None;
|
||||
let mut default_tz = Tz::UTC;
|
||||
let mut is_subscribed = is_account_owner;
|
||||
for calendar_id in resource
|
||||
.child_names()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|n| n.parent_id)
|
||||
{
|
||||
match preferences_cache.entry(calendar_id) {
|
||||
Entry::Occupied(e) => {
|
||||
if let Some(prefs) = e.get() {
|
||||
default_tz = prefs.tz;
|
||||
is_subscribed |= prefs.flags & CALENDAR_SUBSCRIBED != 0;
|
||||
include_in_availability =
|
||||
IncludeInAvailability::from_flags(prefs.flags);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(e) => {
|
||||
if let Some(prefs) = resources
|
||||
.container_resource_by_id(calendar_id)
|
||||
.and_then(|r| r.calendar_preferences(principal_id))
|
||||
{
|
||||
default_tz = prefs.tz;
|
||||
is_subscribed |= prefs.flags & CALENDAR_SUBSCRIBED != 0;
|
||||
include_in_availability =
|
||||
IncludeInAvailability::from_flags(prefs.flags);
|
||||
e.insert(Some(prefs));
|
||||
} else {
|
||||
e.insert(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let include_in_availability = include_in_availability.unwrap_or({
|
||||
if is_account_owner {
|
||||
IncludeInAvailability::All
|
||||
} else {
|
||||
IncludeInAvailability::None
|
||||
}
|
||||
});
|
||||
|
||||
if !is_subscribed || include_in_availability == IncludeInAvailability::None {
|
||||
continue 'next_event;
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let document_id = resource.document_id;
|
||||
let Some(archive) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let event = archive
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Find the component ids that match the criteria
|
||||
let mut matching_component_ids = AHashMap::new();
|
||||
'next_component: for (component_id, component) in
|
||||
event.data.event.components.iter().enumerate()
|
||||
{
|
||||
if !component.component_type.is_event_or_todo() {
|
||||
continue 'next_component;
|
||||
}
|
||||
|
||||
let mut is_cancelled = false;
|
||||
let mut is_main_event = true;
|
||||
let mut busy_status = None;
|
||||
|
||||
for entry in component.entries.iter() {
|
||||
match (&entry.name, entry.values.first()) {
|
||||
(
|
||||
ArchivedICalendarProperty::Status,
|
||||
Some(ArchivedICalendarValue::Status(
|
||||
ArchivedICalendarStatus::Cancelled,
|
||||
)),
|
||||
) => {
|
||||
// The "status" property of the event is not "cancelled".
|
||||
is_cancelled = true;
|
||||
}
|
||||
(ArchivedICalendarProperty::RecurrenceId, _) => {
|
||||
is_main_event = false;
|
||||
}
|
||||
(
|
||||
ArchivedICalendarProperty::Class,
|
||||
Some(ArchivedICalendarValue::Classification(
|
||||
ArchivedICalendarClassification::Confidential,
|
||||
)),
|
||||
) => {
|
||||
// Condition: The event's "privacy" property is not "secret".
|
||||
continue 'next_component;
|
||||
}
|
||||
(
|
||||
ArchivedICalendarProperty::Transp,
|
||||
Some(ArchivedICalendarValue::Transparency(
|
||||
ArchivedICalendarTransparency::Transparent,
|
||||
)),
|
||||
) => {
|
||||
// Condition: The "freeBusyStatus" property of the event is "busy" (or omitted, as this is the default).
|
||||
continue 'next_component;
|
||||
}
|
||||
(ArchivedICalendarProperty::Attendee, Some(value))
|
||||
if include_in_availability == IncludeInAvailability::Attending =>
|
||||
{
|
||||
if let Some(attendee) = value.as_text().and_then(|attendee| {
|
||||
sanitize_email(strip_mailto_scheme(attendee))
|
||||
}) {
|
||||
// Condition: the Principal is a participant of the event, and has a "participationStatus" of "accepted" or "tentative".
|
||||
if principal_account.addresses().contains(&attendee) {
|
||||
busy_status = Some(
|
||||
entry
|
||||
.parameters(&ICalendarParameterName::Partstat)
|
||||
.next()
|
||||
.map(|v| {
|
||||
match v {
|
||||
ArchivedICalendarParameterValue::Partstat(
|
||||
ArchivedICalendarParticipationStatus::Accepted,
|
||||
) => BusyStatus::Confirmed,
|
||||
ArchivedICalendarParameterValue::Partstat(
|
||||
ArchivedICalendarParticipationStatus::Tentative,
|
||||
) => BusyStatus::Tentative,
|
||||
ArchivedICalendarParameterValue::Partstat(
|
||||
ArchivedICalendarParticipationStatus::Declined,
|
||||
) => {
|
||||
is_cancelled = true;
|
||||
BusyStatus::Unavailable
|
||||
}
|
||||
_ => BusyStatus::Unavailable,
|
||||
}
|
||||
})
|
||||
.unwrap_or(BusyStatus::Unavailable),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if is_cancelled {
|
||||
if is_main_event {
|
||||
continue 'next_event;
|
||||
} else {
|
||||
continue 'next_component;
|
||||
}
|
||||
}
|
||||
|
||||
let busy_status = if let Some(busy_status) = busy_status {
|
||||
busy_status
|
||||
} else if include_in_availability == IncludeInAvailability::All {
|
||||
BusyStatus::Confirmed
|
||||
} else {
|
||||
continue 'next_component;
|
||||
};
|
||||
|
||||
matching_component_ids.insert(component_id as u32, busy_status);
|
||||
}
|
||||
|
||||
if matching_component_ids.is_empty() {
|
||||
// No events matched the criteria
|
||||
continue 'next_event;
|
||||
}
|
||||
|
||||
for expansion in event.data.expand(default_tz, filter).unwrap_or_default() {
|
||||
let Some(busy_status) = matching_component_ids.get(&expansion.comp_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(recurrence_key) = expansion.recurrence_key() else {
|
||||
continue;
|
||||
};
|
||||
if periods.len() < max_instances {
|
||||
periods.push(FreeBusyResult {
|
||||
utc_start: expansion.start,
|
||||
utc_end: expansion.end,
|
||||
busy_status: *busy_status,
|
||||
recurrence_key,
|
||||
document_id,
|
||||
});
|
||||
} else {
|
||||
return Err(trc::JmapEvent::RequestTooLarge
|
||||
.into_err()
|
||||
.details("The number of expanded instances exceeds the server limit"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = GetAvailabilityResponse {
|
||||
list: Vec::with_capacity(periods.len()),
|
||||
};
|
||||
|
||||
if periods.is_empty() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Sort by busy status and start time
|
||||
periods.sort_unstable_by(|a, b| {
|
||||
a.busy_status
|
||||
.cmp(&b.busy_status)
|
||||
.then_with(|| a.utc_start.cmp(&b.utc_start))
|
||||
});
|
||||
|
||||
if return_event_details {
|
||||
for period in periods {
|
||||
result.list.push(period.into());
|
||||
}
|
||||
} else {
|
||||
// Merge intervals with same busy status
|
||||
let mut start_time = periods[0].utc_start;
|
||||
let mut end_time = periods[0].utc_end;
|
||||
let mut current_status = periods[0].busy_status;
|
||||
|
||||
for curr in periods.iter().skip(1) {
|
||||
if curr.utc_start <= end_time && curr.busy_status == current_status {
|
||||
end_time = end_time.max(curr.utc_end);
|
||||
} else {
|
||||
result.list.push(BusyPeriod {
|
||||
utc_start: UTCDate::from_timestamp(start_time),
|
||||
utc_end: UTCDate::from_timestamp(end_time),
|
||||
busy_status: Some(current_status),
|
||||
event: None,
|
||||
});
|
||||
start_time = curr.utc_start;
|
||||
end_time = curr.utc_end;
|
||||
current_status = curr.busy_status;
|
||||
}
|
||||
}
|
||||
|
||||
result.list.push(BusyPeriod {
|
||||
utc_start: UTCDate::from_timestamp(start_time),
|
||||
utc_end: UTCDate::from_timestamp(end_time),
|
||||
busy_status: Some(current_status),
|
||||
event: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct FreeBusyResult {
|
||||
utc_start: i64,
|
||||
utc_end: i64,
|
||||
busy_status: BusyStatus,
|
||||
recurrence_key: RecurrenceKey,
|
||||
document_id: u32,
|
||||
}
|
||||
|
||||
impl From<FreeBusyResult> for BusyPeriod {
|
||||
fn from(value: FreeBusyResult) -> Self {
|
||||
BusyPeriod {
|
||||
utc_start: UTCDate::from_timestamp(value.utc_start),
|
||||
utc_end: UTCDate::from_timestamp(value.utc_end),
|
||||
busy_status: Some(value.busy_status),
|
||||
event: JSCalendar(Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Property(JSCalendarProperty::Id),
|
||||
Value::Element(JSCalendarValue::Id(<Id as CalendarSyntheticId>::new(
|
||||
value.recurrence_key,
|
||||
value.document_id,
|
||||
))),
|
||||
),
|
||||
(
|
||||
Key::Property(JSCalendarProperty::BaseEventId),
|
||||
Value::Element(JSCalendarValue::Id(Id::from(value.document_id))),
|
||||
),
|
||||
])))
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::principal::{Principal, PrincipalProperty, PrincipalType, PrincipalValue},
|
||||
request::capability::Capability,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use registry::schema::prelude::{ObjectType, Permission};
|
||||
use std::future::Future;
|
||||
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
|
||||
use trc::AddContext;
|
||||
|
||||
pub trait PrincipalGet: Sync + Send {
|
||||
fn principal_get(
|
||||
&self,
|
||||
request: GetRequest<Principal>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<Principal>>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalGet for Server {
|
||||
async fn principal_get(
|
||||
&self,
|
||||
mut request: GetRequest<Principal>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<Principal>> {
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::JmapPrincipalGet)
|
||||
{
|
||||
return Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("The administrator has disabled directory queries.".to_string()));
|
||||
}
|
||||
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
PrincipalProperty::Id,
|
||||
PrincipalProperty::Type,
|
||||
PrincipalProperty::Name,
|
||||
PrincipalProperty::Description,
|
||||
PrincipalProperty::Email,
|
||||
]);
|
||||
|
||||
// Return all principals
|
||||
let principal_ids = self
|
||||
.registry()
|
||||
.query::<RoaringBitmap>(
|
||||
RegistryQuery::new(ObjectType::Account).with_tenant(access_token.tenant_id()),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
principal_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: State::Initial.into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the principal
|
||||
let document_id = id.document_id();
|
||||
if !principal_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let principal = self
|
||||
.account(document_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
let value = match property {
|
||||
PrincipalProperty::Id => Value::Element(PrincipalValue::Id(id)),
|
||||
PrincipalProperty::Type => {
|
||||
Value::Element(PrincipalValue::Type(if principal.is_user_account() {
|
||||
PrincipalType::Individual
|
||||
} else {
|
||||
PrincipalType::Group
|
||||
}))
|
||||
}
|
||||
PrincipalProperty::Name => Value::Str(principal.name().to_string().into()),
|
||||
PrincipalProperty::Description => principal
|
||||
.description()
|
||||
.map(|v| Value::Str(v.to_string().into()))
|
||||
.unwrap_or(Value::Null),
|
||||
PrincipalProperty::Email => Value::Str(principal.name().to_string().into()),
|
||||
PrincipalProperty::Accounts => Value::Object(Map::from(vec![(
|
||||
Key::Property(PrincipalProperty::IdValue(id)),
|
||||
Value::Object(Map::from_iter(
|
||||
[
|
||||
Capability::Mail,
|
||||
Capability::Contacts,
|
||||
Capability::Calendars,
|
||||
Capability::FileNode,
|
||||
Capability::Principals,
|
||||
]
|
||||
.iter()
|
||||
.map(|cap| {
|
||||
(
|
||||
Key::Property(PrincipalProperty::Capability(*cap)),
|
||||
Value::Object(Map::new()),
|
||||
)
|
||||
})
|
||||
.chain([
|
||||
(
|
||||
Key::Property(PrincipalProperty::Capability(
|
||||
Capability::PrincipalsOwner,
|
||||
)),
|
||||
Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Borrowed("accountIdForPrincipal"),
|
||||
Value::Element(PrincipalValue::Id(id)),
|
||||
),
|
||||
(
|
||||
Key::Borrowed("principalId"),
|
||||
Value::Element(PrincipalValue::Id(id)),
|
||||
),
|
||||
])),
|
||||
),
|
||||
(
|
||||
Key::Property(PrincipalProperty::Capability(
|
||||
Capability::Calendars,
|
||||
)),
|
||||
Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Borrowed("accountId"),
|
||||
Value::Element(PrincipalValue::Id(id)),
|
||||
),
|
||||
(Key::Borrowed("mayGetAvailability"), Value::Bool(true)),
|
||||
(Key::Borrowed("mayShareWith"), Value::Bool(true)),
|
||||
(
|
||||
Key::Borrowed("calendarAddress"),
|
||||
Value::Str(
|
||||
format!("mailto:{}", principal.name()).into(),
|
||||
),
|
||||
),
|
||||
])),
|
||||
),
|
||||
]),
|
||||
)),
|
||||
)])),
|
||||
PrincipalProperty::Capabilities => Value::Object(Map::from_iter(
|
||||
[
|
||||
Capability::Mail,
|
||||
Capability::Contacts,
|
||||
Capability::Calendars,
|
||||
Capability::FileNode,
|
||||
Capability::Principals,
|
||||
]
|
||||
.iter()
|
||||
.map(|cap| {
|
||||
(
|
||||
Key::Property(PrincipalProperty::Capability(*cap)),
|
||||
Value::Object(Map::new()),
|
||||
)
|
||||
}),
|
||||
)),
|
||||
_ => Value::Null,
|
||||
};
|
||||
|
||||
result.insert_unchecked(property.clone(), value);
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod availability;
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::api::query::QueryResponseBuilder;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::principal::{Principal, PrincipalFilter, PrincipalType},
|
||||
types::state::State,
|
||||
};
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::AccountType,
|
||||
prelude::{ObjectType, Permission, Property},
|
||||
},
|
||||
types::EnumImpl,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
registry::RegistryQuery,
|
||||
roaring::RoaringBitmap,
|
||||
search::{SearchFilter, SearchQuery},
|
||||
write::SearchIndex,
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
pub trait PrincipalQuery: Sync + Send {
|
||||
fn principal_query(
|
||||
&self,
|
||||
request: QueryRequest<Principal>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
impl PrincipalQuery for Server {
|
||||
async fn principal_query(
|
||||
&self,
|
||||
mut request: QueryRequest<Principal>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
if !self.core.groupware.allow_directory_query
|
||||
&& !access_token.has_permission(Permission::JmapPrincipalQuery)
|
||||
{
|
||||
return Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("The administrator has disabled directory queries.".to_string()));
|
||||
}
|
||||
|
||||
let principal_ids = self
|
||||
.registry()
|
||||
.query::<RoaringBitmap>(
|
||||
RegistryQuery::new(ObjectType::Account).with_tenant(access_token.tenant_id()),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
PrincipalFilter::Name(name) | PrincipalFilter::Email(name) => {
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
match self.account_id_from_email(&name, false).await? {
|
||||
Some(account_id) => {
|
||||
RoaringBitmap::from_sorted_iter([account_id]).unwrap()
|
||||
}
|
||||
None => RoaringBitmap::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
PrincipalFilter::AccountIds(ids) => {
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| {
|
||||
let id = id.document_id();
|
||||
if principal_ids.contains(id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<RoaringBitmap>(),
|
||||
));
|
||||
}
|
||||
PrincipalFilter::Text(text) => {
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
self.registry()
|
||||
.query::<RoaringBitmap>(
|
||||
RegistryQuery::new(ObjectType::Account)
|
||||
.with_tenant(access_token.tenant_id())
|
||||
.text(Property::Text, text),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
));
|
||||
}
|
||||
PrincipalFilter::Type(principal_type) => {
|
||||
let typ = match principal_type {
|
||||
PrincipalType::Individual => AccountType::User,
|
||||
PrincipalType::Group => AccountType::Group,
|
||||
_ => {
|
||||
filters.push(SearchFilter::is_in_set(Default::default()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
self.registry()
|
||||
.query::<RoaringBitmap>(
|
||||
RegistryQuery::new(ObjectType::Account)
|
||||
.equal(Property::Type, typ.to_id())
|
||||
.with_tenant(access_token.tenant_id()),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
));
|
||||
}
|
||||
other => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(other.to_string()));
|
||||
}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let results = SearchQuery::new(SearchIndex::InMemory)
|
||||
.with_filters(filters)
|
||||
.with_mask(principal_ids)
|
||||
.filter()
|
||||
.into_bitmap();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user