/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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, expand::RecurrenceKey}, }; use jmap_proto::{ method::query::{Filter, QueryRequest, QueryResponse}, object::{ calendar, calendar_event::{self, CalendarEventComparator, CalendarEventFilter}, }, request::MaybeInvalid, types::state::State, }; use nlp::language::Language; use std::{cmp::Ordering, sync::Arc}; use store::{ ValueKey, roaring::RoaringBitmap, search::{CalendarSearchField, SearchComparator, SearchFilter, SearchQuery}, write::{AlignedBytes, Archive, SearchIndex}, }; use trc::AddContext; use types::{ TimeRange, acl::Acl, collection::{Collection, SyncCollection}, }; pub trait CalendarEventQuery: Sync + Send { fn calendar_event_query( &self, request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; fn calendar_query( &self, request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; } impl CalendarEventQuery for Server { async fn calendar_event_query( &self, mut request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { 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(), account_id, SyncCollection::Calendar, ) .await?; let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC); let mut filter: Option = 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.email.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.email.default_language, )); } CalendarEventFilter::Description(description) => { filters.push(SearchFilter::has_text_detect( CalendarSearchField::Description, description, self.core.email.default_language, )); } CalendarEventFilter::Location(location) => { filters.push(SearchFilter::has_text_detect( CalendarSearchField::Location, location, self.core.email.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::, _>>()? } 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 .store() .get_value::>(ValueKey::archive( account_id, Collection::CalendarEvent, document_id, )) .await? else { continue; }; let calendar_event = _calendar_event .unarchive::() .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() { let Some(recurrence_key) = expansion.recurrence_key() else { continue; }; 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, recurrence_key, }); } 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.recurrence_key.prefix(), 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() } } async fn calendar_query( &self, request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources( access_token.account_id(), account_id, SyncCollection::Calendar, ) .await?; let results = cache.document_ids(true).collect::>(); 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() } } fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option { tz.from_local_datetime(&dt.to_naive_date_time()?) .single() .map(|dt| dt.timestamp()) } #[derive(Debug)] struct SearchResult { recurrence_key: RecurrenceKey, document_id: u32, start: [u8; std::mem::size_of::()], created: [u8; std::mem::size_of::()], updated: [u8; std::mem::size_of::()], uid: Arc, } 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(_) => &[], } } }