Files
Stalwart/crates/jmap/src/calendar_event/query.rs
2025-11-12 19:09:06 +01:00

387 lines
16 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use calcard::{common::timezone::Tz, jscalendar::JSCalendarDateTime};
use chrono::offset::TimeZone;
use common::{Server, auth::AccessToken};
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
use jmap_proto::{
method::query::{Filter, QueryRequest, QueryResponse},
object::calendar_event::{self, CalendarEventComparator, CalendarEventFilter},
request::MaybeInvalid,
};
use nlp::language::Language;
use std::{cmp::Ordering, sync::Arc};
use store::{
roaring::RoaringBitmap,
search::{CalendarSearchField, SearchComparator, SearchFilter, SearchQuery},
write::SearchIndex,
};
use trc::AddContext;
use types::{
TimeRange,
acl::Acl,
collection::{Collection, SyncCollection},
};
pub trait CalendarEventQuery: Sync + Send {
fn calendar_event_query(
&self,
request: QueryRequest<calendar_event::CalendarEvent>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
impl CalendarEventQuery for Server {
async fn calendar_event_query(
&self,
mut request: QueryRequest<calendar_event::CalendarEvent>,
access_token: &AccessToken,
) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len());
let cache = self
.fetch_dav_resources(access_token, account_id, SyncCollection::Calendar)
.await?;
let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC);
let mut filter: Option<TimeRange> = None;
// Extract from/to arguments
for cond in &request.filter {
if let Filter::Property(CalendarEventFilter::After(date)) = cond {
if let Some(after) = local_timestamp(date, default_tz) {
filter.get_or_insert_default().start = after;
}
} else if let Filter::Property(CalendarEventFilter::Before(date)) = cond
&& let Some(before) = local_timestamp(date, default_tz)
{
filter.get_or_insert_default().end = before;
}
}
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => match cond {
CalendarEventFilter::InCalendar(MaybeInvalid::Value(id)) => {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cache.children_ids(id.document_id()),
)))
}
CalendarEventFilter::Uid(uid) => {
filters.push(SearchFilter::eq(CalendarSearchField::Uid, uid));
}
CalendarEventFilter::Text(value) => {
let (text, language) =
Language::detect(value, self.core.jmap.default_language);
filters.push(SearchFilter::Or);
filters.push(SearchFilter::has_text(
CalendarSearchField::Title,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Description,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Location,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Owner,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Attendee,
text,
language,
));
filters.push(SearchFilter::End);
}
CalendarEventFilter::Title(title) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Title,
title,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Description(description) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Description,
description,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Location(location) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Location,
location,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Owner(owner) => {
filters.push(SearchFilter::has_text(
CalendarSearchField::Owner,
owner,
Language::None,
));
}
CalendarEventFilter::Attendee(attendee) => {
filters.push(SearchFilter::has_text(
CalendarSearchField::Attendee,
attendee,
Language::None,
));
}
CalendarEventFilter::After(after) => {
/*
The end of the event, or any recurrence of the event, in the time zone given
as the "timeZone" argument, must be after this date to match the condition.
*/
if let Some(after) = local_timestamp(&after, default_tz) {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cache.resources.iter().filter_map(|r| {
r.event_time_range()
.and_then(|(_, end)| (after < end).then_some(r.document_id))
}),
)));
}
}
CalendarEventFilter::Before(before) => {
/*
The start of the event, or any recurrence of the event, in the time zone given
as the "timeZone" argument, must be before this date to match the condition.
*/
if let Some(before) = local_timestamp(&before, default_tz) {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
cache.resources.iter().filter_map(|r| {
r.event_time_range().and_then(|(start, _)| {
(before > start).then_some(r.document_id)
})
}),
)));
}
}
unsupported => {
return Err(trc::JmapEvent::UnsupportedFilter
.into_err()
.details(unsupported.into_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 expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
let comparators = if !expand_recurrences {
request
.sort
.take()
.unwrap_or_default()
.into_iter()
.map(|comparator| match comparator.property {
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
Ok(SearchComparator::field(
CalendarSearchField::Start,
comparator.is_ascending,
))
}
CalendarEventComparator::Uid => Ok(SearchComparator::field(
CalendarSearchField::Uid,
comparator.is_ascending,
)),
CalendarEventComparator::Created | CalendarEventComparator::Updated => {
Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(comparator.property.into_string().into_owned()))
}
CalendarEventComparator::_T(other) => Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(other.to_string())),
})
.collect::<Result<Vec<_>, _>>()?
} else {
vec![]
};
let results = self
.search_store()
.query_account(
SearchQuery::new(SearchIndex::Calendar)
.with_filters(filters)
.with_comparators(comparators)
.with_account_id(account_id)
.with_mask(if access_token.is_shared(account_id) {
cache.shared_items(access_token, [Acl::ReadItems], true)
} else {
cache.document_ids(false).collect()
}),
)
.await?;
// Extract comparators
let comparators = request
.sort
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or_default();
if expand_recurrences && !results.is_empty() {
let Some(time_range) = filter.filter(|f| f.start != i64::MIN && f.end != i64::MAX)
else {
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
"Both 'after' and 'before' filters are required when expanding recurrences",
));
};
let max_instances = self.core.groupware.max_ical_instances;
let mut expanded_results = Vec::with_capacity(results.len() as usize);
let has_uid_comparator = comparators
.iter()
.any(|c| matches!(c.property, CalendarEventComparator::Uid));
for document_id in results {
let Some(_calendar_event) = self
.archive(account_id, Collection::CalendarEvent, document_id)
.await?
else {
continue;
};
let calendar_event = _calendar_event
.unarchive::<CalendarEvent>()
.caused_by(trc::location!())?;
// Expand recurrences
let uid = if has_uid_comparator {
Arc::new(
calendar_event
.data
.event
.uids()
.next()
.unwrap_or_default()
.to_string(),
)
} else {
Arc::new(String::new())
};
for expansion in calendar_event
.data
.expand(default_tz, time_range)
.unwrap_or_default()
{
if expanded_results.len() < max_instances {
expanded_results.push(SearchResult {
created: calendar_event.created.to_native().to_be_bytes(),
updated: calendar_event.modified.to_native().to_be_bytes(),
start: expansion.start.to_be_bytes(),
uid: uid.clone(),
document_id,
expansion_id: expansion.expansion_id.into(),
});
} else {
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
"The number of expanded recurrences exceeds the server limit",
));
}
}
}
let mut response = QueryResponseBuilder::new(
expanded_results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
// Sort results
if !expanded_results.is_empty() {
expanded_results.sort_by(|a, b| {
for comparator in comparators {
let ordering = if comparator.is_ascending {
a.get_property(&comparator.property)
.cmp(b.get_property(&comparator.property))
} else {
b.get_property(&comparator.property)
.cmp(a.get_property(&comparator.property))
};
if ordering != Ordering::Equal {
return ordering;
}
}
Ordering::Equal
});
// Add results
for result in expanded_results {
if !response.add(result.expansion_id.unwrap() + 1, result.document_id) {
break;
}
}
}
response.build()
} else {
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
for document_id in results {
if !response.add(0, document_id) {
break;
}
}
response.build()
}
}
}
fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option<i64> {
tz.from_local_datetime(&dt.to_naive_date_time()?)
.single()
.map(|dt| dt.timestamp())
}
#[derive(Debug)]
struct SearchResult {
expansion_id: Option<u32>,
document_id: u32,
start: [u8; std::mem::size_of::<i64>()],
created: [u8; std::mem::size_of::<i64>()],
updated: [u8; std::mem::size_of::<i64>()],
uid: Arc<String>,
}
impl SearchResult {
fn get_property(&self, comparator: &CalendarEventComparator) -> &[u8] {
match comparator {
CalendarEventComparator::Uid => self.uid.as_bytes(),
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
self.start.as_ref()
}
CalendarEventComparator::Created => self.created.as_ref(),
CalendarEventComparator::Updated => self.updated.as_ref(),
CalendarEventComparator::_T(_) => &[],
}
}
}