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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+434
View File
@@ -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,
})
}
+9
View File
@@ -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;
+320
View File
@@ -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![],
}
}
+93
View File
@@ -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
})
}
}