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,434 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::auth::JmapAuthorization, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::changes::{ChangesRequest, ChangesResponse},
|
||||
object::{JmapObject, NullObject, mailbox::MailboxProperty},
|
||||
request::method::MethodObject,
|
||||
response::{ChangesResponseMethod, ResponseMethod},
|
||||
types::state::State,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
query::log::{Change, Query},
|
||||
roaring::RoaringBitmap,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub trait ChangesLookup: Sync + Send {
|
||||
fn changes(
|
||||
&self,
|
||||
request: ChangesRequest,
|
||||
object: MethodObject,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<IntermediateChangesResponse>> + Send;
|
||||
}
|
||||
|
||||
pub struct IntermediateChangesResponse {
|
||||
pub response: ChangesResponse<NullObject>,
|
||||
pub object: MethodObject,
|
||||
pub only_container_changes: bool,
|
||||
}
|
||||
|
||||
impl ChangesLookup for Server {
|
||||
async fn changes(
|
||||
&self,
|
||||
request: ChangesRequest,
|
||||
object: MethodObject,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<IntermediateChangesResponse> {
|
||||
// Map collection and validate ACLs
|
||||
let (collection, is_container) = match object {
|
||||
MethodObject::Email => {
|
||||
access_token.assert_has_access(request.account_id, Collection::Email)?;
|
||||
(SyncCollection::Email, false)
|
||||
}
|
||||
MethodObject::Mailbox => {
|
||||
access_token.assert_has_access(request.account_id, Collection::Mailbox)?;
|
||||
|
||||
(SyncCollection::Email, true)
|
||||
}
|
||||
MethodObject::Thread => {
|
||||
access_token.assert_has_access(request.account_id, Collection::Email)?;
|
||||
|
||||
(SyncCollection::Thread, true)
|
||||
}
|
||||
MethodObject::Identity => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
(SyncCollection::Identity, false)
|
||||
}
|
||||
MethodObject::EmailSubmission => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
(SyncCollection::EmailSubmission, false)
|
||||
}
|
||||
MethodObject::AddressBook => {
|
||||
access_token.assert_has_access(request.account_id, Collection::AddressBook)?;
|
||||
|
||||
(SyncCollection::AddressBook, true)
|
||||
}
|
||||
MethodObject::ContactCard => {
|
||||
access_token.assert_has_access(request.account_id, Collection::ContactCard)?;
|
||||
|
||||
(SyncCollection::AddressBook, false)
|
||||
}
|
||||
MethodObject::FileNode => {
|
||||
access_token.assert_has_access(request.account_id, Collection::FileNode)?;
|
||||
|
||||
(SyncCollection::FileNode, false)
|
||||
}
|
||||
MethodObject::Calendar => {
|
||||
access_token.assert_has_access(request.account_id, Collection::Calendar)?;
|
||||
|
||||
(SyncCollection::Calendar, true)
|
||||
}
|
||||
MethodObject::CalendarEvent => {
|
||||
access_token.assert_has_access(request.account_id, Collection::CalendarEvent)?;
|
||||
|
||||
(SyncCollection::Calendar, false)
|
||||
}
|
||||
MethodObject::CalendarEventNotification => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
(SyncCollection::CalendarEventNotification, false)
|
||||
}
|
||||
MethodObject::ShareNotification => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
(SyncCollection::ShareNotification, false)
|
||||
}
|
||||
_ => {
|
||||
return Err(trc::JmapEvent::CannotCalculateChanges.into_err());
|
||||
}
|
||||
};
|
||||
let max_changes = std::cmp::min(
|
||||
request
|
||||
.max_changes
|
||||
.filter(|n| *n != 0)
|
||||
.unwrap_or(usize::MAX),
|
||||
self.core.jmap.changes_max_results,
|
||||
);
|
||||
let mut response: ChangesResponse<NullObject> = ChangesResponse {
|
||||
account_id: request.account_id,
|
||||
old_state: request.since_state.clone(),
|
||||
new_state: State::Initial,
|
||||
has_more_changes: false,
|
||||
created: vec![],
|
||||
updated: vec![],
|
||||
destroyed: vec![],
|
||||
updated_properties: None,
|
||||
};
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
let allowed_ids: Option<RoaringBitmap> = if access_token.is_member(account_id) {
|
||||
None
|
||||
} else {
|
||||
Some(match object {
|
||||
MethodObject::Email => self
|
||||
.get_cached_messages(account_id)
|
||||
.await?
|
||||
.shared_messages(access_token, Acl::ReadItems),
|
||||
MethodObject::Mailbox => self
|
||||
.get_cached_messages(account_id)
|
||||
.await?
|
||||
.shared_mailboxes(access_token, Acl::Read),
|
||||
MethodObject::Thread => {
|
||||
let cache = self.get_cached_messages(account_id).await?;
|
||||
let shared = cache.shared_messages(access_token, Acl::ReadItems);
|
||||
let mut threads = RoaringBitmap::new();
|
||||
for item in &cache.emails.items {
|
||||
if shared.contains(item.document_id) {
|
||||
threads.insert(item.thread_id);
|
||||
}
|
||||
}
|
||||
threads
|
||||
}
|
||||
MethodObject::AddressBook => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?
|
||||
.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true),
|
||||
MethodObject::ContactCard => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?
|
||||
.shared_items(access_token, [Acl::ReadItems], true),
|
||||
MethodObject::Calendar => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await?
|
||||
.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true),
|
||||
MethodObject::CalendarEvent => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await?
|
||||
.shared_items(access_token, [Acl::ReadItems], true),
|
||||
MethodObject::FileNode => self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::FileNode,
|
||||
)
|
||||
.await?
|
||||
.shared_documents(access_token, [Acl::Read, Acl::ReadItems], true),
|
||||
_ => RoaringBitmap::new(),
|
||||
})
|
||||
};
|
||||
|
||||
let (items_sent, changelog) = match &request.since_state {
|
||||
State::Initial => {
|
||||
let changelog = self
|
||||
.store()
|
||||
.changes(account_id, collection.into(), Query::All)
|
||||
.await?;
|
||||
if changelog.changes.is_empty() && changelog.from_change_id == 0 {
|
||||
return Ok(IntermediateChangesResponse {
|
||||
response,
|
||||
object,
|
||||
only_container_changes: false,
|
||||
});
|
||||
}
|
||||
|
||||
(0, changelog)
|
||||
}
|
||||
State::Exact(change_id) => {
|
||||
let last_state = match collection {
|
||||
SyncCollection::Calendar | SyncCollection::AddressBook => self
|
||||
.fetch_dav_resources(access_token.account_id(), account_id, collection)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.get_state(is_container)
|
||||
.into(),
|
||||
SyncCollection::Email => self
|
||||
.get_cached_messages(account_id)
|
||||
.await?
|
||||
.get_state(is_container)
|
||||
.into(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(last_state) = last_state {
|
||||
response.new_state = last_state;
|
||||
|
||||
if response.new_state == State::Exact(*change_id) {
|
||||
return Ok(IntermediateChangesResponse {
|
||||
response,
|
||||
object,
|
||||
only_container_changes: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
0,
|
||||
self.store()
|
||||
.changes(account_id, collection.into(), Query::Since(*change_id))
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
State::Intermediate(intermediate_state) => {
|
||||
let changelog = self
|
||||
.store()
|
||||
.changes(
|
||||
account_id,
|
||||
collection.into(),
|
||||
Query::RangeInclusive(intermediate_state.from_id, intermediate_state.to_id),
|
||||
)
|
||||
.await?;
|
||||
if (is_container
|
||||
&& intermediate_state.items_sent >= changelog.total_container_changes())
|
||||
|| (!is_container
|
||||
&& intermediate_state.items_sent >= changelog.total_item_changes())
|
||||
{
|
||||
(
|
||||
0,
|
||||
self.store()
|
||||
.changes(
|
||||
account_id,
|
||||
collection.into(),
|
||||
Query::Since(intermediate_state.to_id),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
(intermediate_state.items_sent, changelog)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (changelog.is_truncated || changelog.from_change_id == 0)
|
||||
&& request.since_state != State::Initial
|
||||
{
|
||||
return Err(trc::JmapEvent::CannotCalculateChanges.into_err().details(
|
||||
if changelog.is_truncated {
|
||||
"Change log is truncated"
|
||||
} else {
|
||||
"Since state is invalid"
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut changes = changelog
|
||||
.changes
|
||||
.into_iter()
|
||||
.filter(|change| {
|
||||
(is_container && change.is_container_change())
|
||||
|| (!is_container && change.is_item_change())
|
||||
})
|
||||
.filter(|change| {
|
||||
allowed_ids.as_ref().is_none_or(|allowed| {
|
||||
let id = if is_container {
|
||||
change.container_id()
|
||||
} else {
|
||||
change.item_id()
|
||||
};
|
||||
id.is_some_and(|id| allowed.contains(id as u32))
|
||||
})
|
||||
})
|
||||
.skip(items_sent)
|
||||
.peekable();
|
||||
|
||||
let mut items_changed = false;
|
||||
for change in (&mut changes).take(max_changes) {
|
||||
match change {
|
||||
Change::InsertContainer(item) | Change::InsertItem(item) => {
|
||||
response.created.push(item.into());
|
||||
}
|
||||
Change::UpdateContainer(item) | Change::UpdateItem(item) => {
|
||||
response.updated.push(item.into());
|
||||
items_changed = true;
|
||||
}
|
||||
Change::DeleteContainer(item) | Change::DeleteItem(item) => {
|
||||
response.destroyed.push(item.into());
|
||||
}
|
||||
Change::UpdateContainerProperty(item) => {
|
||||
response.updated.push(item.into());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let change_id = (if is_container {
|
||||
changelog.container_change_id
|
||||
} else {
|
||||
changelog.item_change_id
|
||||
})
|
||||
.unwrap_or(changelog.to_change_id);
|
||||
|
||||
response.has_more_changes = changes.peek().is_some();
|
||||
if response.has_more_changes {
|
||||
response.new_state = State::new_intermediate(
|
||||
changelog.from_change_id,
|
||||
change_id,
|
||||
items_sent + max_changes,
|
||||
);
|
||||
} else if response.new_state == State::Initial {
|
||||
response.new_state = State::new_exact(change_id)
|
||||
}
|
||||
|
||||
Ok(IntermediateChangesResponse {
|
||||
only_container_changes: is_container && !response.updated.is_empty() && !items_changed,
|
||||
response,
|
||||
object,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntermediateChangesResponse {
|
||||
pub fn into_method_response(self) -> ResponseMethod<'static> {
|
||||
ResponseMethod::Changes(match self.object {
|
||||
MethodObject::Email => ChangesResponseMethod::Email(transmute_response(self.response)),
|
||||
MethodObject::Mailbox => {
|
||||
let mut response = transmute_response(self.response);
|
||||
if self.only_container_changes {
|
||||
response.updated_properties = vec![
|
||||
MailboxProperty::TotalEmails.into(),
|
||||
MailboxProperty::UnreadEmails.into(),
|
||||
MailboxProperty::TotalThreads.into(),
|
||||
MailboxProperty::UnreadThreads.into(),
|
||||
]
|
||||
.into();
|
||||
}
|
||||
ChangesResponseMethod::Mailbox(response)
|
||||
}
|
||||
MethodObject::Thread => {
|
||||
ChangesResponseMethod::Thread(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::Identity => {
|
||||
ChangesResponseMethod::Identity(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::EmailSubmission => {
|
||||
ChangesResponseMethod::EmailSubmission(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::AddressBook => {
|
||||
ChangesResponseMethod::AddressBook(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::ContactCard => {
|
||||
ChangesResponseMethod::ContactCard(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::FileNode => {
|
||||
ChangesResponseMethod::FileNode(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::Calendar => {
|
||||
ChangesResponseMethod::Calendar(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::CalendarEvent => {
|
||||
ChangesResponseMethod::CalendarEvent(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::CalendarEventNotification => {
|
||||
ChangesResponseMethod::CalendarEventNotification(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::ShareNotification => {
|
||||
ChangesResponseMethod::ShareNotification(transmute_response(self.response))
|
||||
}
|
||||
MethodObject::ParticipantIdentity
|
||||
| MethodObject::Core
|
||||
| MethodObject::Blob
|
||||
| MethodObject::PushSubscription
|
||||
| MethodObject::SearchSnippet
|
||||
| MethodObject::VacationResponse
|
||||
| MethodObject::SieveScript
|
||||
| MethodObject::Principal
|
||||
| MethodObject::Quota
|
||||
| MethodObject::Registry(_) => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn transmute_response<T: JmapObject>(
|
||||
response: ChangesResponse<NullObject>,
|
||||
) -> Box<ChangesResponse<T>> {
|
||||
Box::new(ChangesResponse {
|
||||
account_id: response.account_id,
|
||||
old_state: response.old_state,
|
||||
new_state: response.new_state,
|
||||
has_more_changes: response.has_more_changes,
|
||||
created: response.created,
|
||||
updated: response.updated,
|
||||
destroyed: response.destroyed,
|
||||
updated_properties: None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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 state;
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::get::ChangesLookup;
|
||||
use crate::{
|
||||
api::request::resolve_account_id, calendar_event::query::CalendarEventQuery,
|
||||
calendar_event_notification::query::CalendarEventNotificationQuery,
|
||||
contact::query::ContactCardQuery, email::query::EmailQuery, file::query::FileNodeQuery,
|
||||
mailbox::query::MailboxQuery, share_notification::query::ShareNotificationQuery,
|
||||
submission::query::EmailSubmissionQuery,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
method::{
|
||||
changes::{ChangesRequest, ChangesResponse},
|
||||
query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse},
|
||||
},
|
||||
object::{JmapObject, NullObject},
|
||||
request::{QueryChangesRequestMethod, method::MethodObject},
|
||||
};
|
||||
use std::future::Future;
|
||||
|
||||
pub trait QueryChanges: Sync + Send {
|
||||
fn query_changes(
|
||||
&self,
|
||||
request: QueryChangesRequestMethod,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryChangesResponse>> + Send;
|
||||
}
|
||||
|
||||
impl QueryChanges for Server {
|
||||
async fn query_changes(
|
||||
&self,
|
||||
request: QueryChangesRequestMethod,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryChangesResponse> {
|
||||
let mut response;
|
||||
let mut is_mutable = true;
|
||||
let results;
|
||||
let changes;
|
||||
let has_changes;
|
||||
let up_to_id;
|
||||
|
||||
match request {
|
||||
QueryChangesRequestMethod::Email(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(&mut request.account_id, MethodObject::Email, access_token)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::Email,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
is_mutable = request.filter.iter().any(|f| !f.is_immutable())
|
||||
|| request
|
||||
.sort
|
||||
.as_ref()
|
||||
.is_some_and(|sort| sort.iter().any(|s| !s.is_immutable()));
|
||||
|
||||
results = self.email_query((*request).into(), access_token).await?;
|
||||
}
|
||||
QueryChangesRequestMethod::Mailbox(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(&mut request.account_id, MethodObject::Mailbox, access_token)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::Mailbox,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self.mailbox_query((*request).into(), access_token).await?;
|
||||
}
|
||||
QueryChangesRequestMethod::EmailSubmission(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::EmailSubmission,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::EmailSubmission,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self.email_submission_query((*request).into()).await?;
|
||||
}
|
||||
QueryChangesRequestMethod::ContactCard(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::ContactCard,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::ContactCard,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self
|
||||
.contact_card_query((*request).into(), access_token)
|
||||
.await?;
|
||||
}
|
||||
QueryChangesRequestMethod::FileNode(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::FileNode,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::FileNode,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self
|
||||
.file_node_query((*request).into(), access_token)
|
||||
.await?;
|
||||
}
|
||||
QueryChangesRequestMethod::CalendarEvent(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::CalendarEvent,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::CalendarEvent,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self
|
||||
.calendar_event_query((*request).into(), access_token)
|
||||
.await?;
|
||||
}
|
||||
QueryChangesRequestMethod::CalendarEventNotification(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::CalendarEventNotification,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::CalendarEventNotification,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self
|
||||
.calendar_event_notification_query((*request).into(), access_token)
|
||||
.await?;
|
||||
}
|
||||
QueryChangesRequestMethod::ShareNotification(mut request) => {
|
||||
// Query changes
|
||||
resolve_account_id(
|
||||
&mut request.account_id,
|
||||
MethodObject::ShareNotification,
|
||||
access_token,
|
||||
)?;
|
||||
changes = self
|
||||
.changes(
|
||||
build_changes_request(&request),
|
||||
MethodObject::ShareNotification,
|
||||
access_token,
|
||||
)
|
||||
.await?
|
||||
.response;
|
||||
let calculate_total = request.calculate_total.unwrap_or(false);
|
||||
has_changes = changes.has_changes();
|
||||
response = build_query_changes_response(&request, &changes);
|
||||
|
||||
if !has_changes && !calculate_total {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
up_to_id = request.up_to_id;
|
||||
results = self.share_notification_query((*request).into()).await?;
|
||||
}
|
||||
QueryChangesRequestMethod::Principal(_) => {
|
||||
return Err(trc::JmapEvent::CannotCalculateChanges.into_err());
|
||||
}
|
||||
QueryChangesRequestMethod::Quota(_) => {
|
||||
return Err(trc::JmapEvent::CannotCalculateChanges.into_err());
|
||||
}
|
||||
}
|
||||
|
||||
if has_changes {
|
||||
if is_mutable {
|
||||
for (index, id) in results.ids.into_iter().enumerate() {
|
||||
if changes.created.contains(&id) || changes.updated.contains(&id) {
|
||||
response.added.push(AddedItem::new(id, index));
|
||||
}
|
||||
}
|
||||
|
||||
response.removed = changes.updated;
|
||||
} else {
|
||||
for (index, id) in results.ids.into_iter().enumerate() {
|
||||
if changes.created.contains(&id) {
|
||||
response.added.push(AddedItem::new(id, index));
|
||||
}
|
||||
if matches!(up_to_id, Some(up_to_id) if up_to_id == id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changes.destroyed.is_empty() {
|
||||
response.removed.extend(changes.destroyed);
|
||||
}
|
||||
}
|
||||
response.total = results.total;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_changes_request<T: JmapObject>(req: &QueryChangesRequest<T>) -> ChangesRequest {
|
||||
ChangesRequest {
|
||||
account_id: req.account_id,
|
||||
since_state: req.since_query_state.clone(),
|
||||
max_changes: req.max_changes,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_query_changes_response<T: JmapObject>(
|
||||
req: &QueryChangesRequest<T>,
|
||||
changes: &ChangesResponse<NullObject>,
|
||||
) -> QueryChangesResponse {
|
||||
QueryChangesResponse {
|
||||
account_id: req.account_id,
|
||||
old_query_state: changes.old_state.clone(),
|
||||
new_query_state: changes.new_state.clone(),
|
||||
total: None,
|
||||
removed: vec![],
|
||||
added: vec![],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{DavResources, MessageStoreCache, Server};
|
||||
use jmap_proto::types::state::State;
|
||||
use std::future::Future;
|
||||
use trc::AddContext;
|
||||
use types::{ChangeId, collection::SyncCollection};
|
||||
|
||||
pub trait StateManager: Sync + Send {
|
||||
fn get_state(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
) -> impl Future<Output = trc::Result<State>> + Send;
|
||||
|
||||
fn assert_state(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
if_in_state: &Option<State>,
|
||||
) -> impl Future<Output = trc::Result<State>> + Send;
|
||||
}
|
||||
|
||||
pub trait JmapCacheState: Sync + Send {
|
||||
fn get_state(&self, is_container: bool) -> State;
|
||||
|
||||
fn assert_state(&self, is_container: bool, if_in_state: &Option<State>) -> trc::Result<State> {
|
||||
let old_state: State = self.get_state(is_container);
|
||||
if let Some(if_in_state) = if_in_state
|
||||
&& &old_state != if_in_state
|
||||
{
|
||||
return Err(trc::JmapEvent::StateMismatch.into_err());
|
||||
}
|
||||
Ok(old_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl StateManager for Server {
|
||||
async fn get_state(&self, account_id: u32, collection: SyncCollection) -> trc::Result<State> {
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.get_last_change_id(account_id, collection.into())
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(State::from)
|
||||
}
|
||||
|
||||
async fn assert_state(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: SyncCollection,
|
||||
if_in_state: &Option<State>,
|
||||
) -> trc::Result<State> {
|
||||
let old_state: State = self.get_state(account_id, collection).await?;
|
||||
if let Some(if_in_state) = if_in_state
|
||||
&& &old_state != if_in_state
|
||||
{
|
||||
return Err(trc::JmapEvent::StateMismatch.into_err());
|
||||
}
|
||||
|
||||
Ok(old_state)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cache_state(change_id: ChangeId) -> State {
|
||||
(change_id != 0).then_some(change_id).into()
|
||||
}
|
||||
|
||||
impl JmapCacheState for MessageStoreCache {
|
||||
fn get_state(&self, is_container: bool) -> State {
|
||||
cache_state(if is_container {
|
||||
self.mailboxes.change_id
|
||||
} else {
|
||||
self.emails.change_id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapCacheState for DavResources {
|
||||
fn get_state(&self, is_container: bool) -> State {
|
||||
cache_state(if is_container {
|
||||
self.container_change_id
|
||||
} else {
|
||||
self.item_change_id
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user