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,178 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::Server;
|
||||
use groupware::calendar::{ParticipantIdentities, ParticipantIdentity};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::participant_identity::{self, ParticipantIdentityProperty, ParticipantIdentityValue},
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::{
|
||||
Serialize, ValueKey,
|
||||
write::{AlignedBytes, Archive, Archiver, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::PrincipalField, id::Id};
|
||||
|
||||
pub trait ParticipantIdentityGet: Sync + Send {
|
||||
fn participant_identity_get(
|
||||
&self,
|
||||
request: GetRequest<participant_identity::ParticipantIdentity>,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<participant_identity::ParticipantIdentity>>> + Send;
|
||||
|
||||
fn participant_identity_get_or_create(
|
||||
&self,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<Option<Archive<AlignedBytes>>>> + Send;
|
||||
}
|
||||
|
||||
impl ParticipantIdentityGet for Server {
|
||||
async fn participant_identity_get(
|
||||
&self,
|
||||
mut request: GetRequest<participant_identity::ParticipantIdentity>,
|
||||
) -> trc::Result<GetResponse<participant_identity::ParticipantIdentity>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
ParticipantIdentityProperty::Id,
|
||||
ParticipantIdentityProperty::Name,
|
||||
ParticipantIdentityProperty::CalendarAddress,
|
||||
ParticipantIdentityProperty::IsDefault,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let identities = self.participant_identity_get_or_create(account_id).await?;
|
||||
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: None,
|
||||
list: Vec::new(),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
let Some(identities) = identities else {
|
||||
for id in ids.unwrap_or_default() {
|
||||
response.push_not_found(id);
|
||||
}
|
||||
return Ok(response);
|
||||
};
|
||||
|
||||
let identities = identities
|
||||
.unarchive::<ParticipantIdentities>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
identities
|
||||
.identities
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(|i| Id::from(i.id.to_native()))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the identity object
|
||||
let document_id = id.document_id();
|
||||
let Some(identity) = identities.identities.iter().find(|i| i.id == document_id) else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
let value = match &property {
|
||||
ParticipantIdentityProperty::Id => {
|
||||
Value::Element(ParticipantIdentityValue::Id(id))
|
||||
}
|
||||
ParticipantIdentityProperty::Name => Value::Str(
|
||||
identity
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|n| n.as_str())
|
||||
.unwrap_or(identities.default_name.as_str())
|
||||
.to_string()
|
||||
.into(),
|
||||
),
|
||||
ParticipantIdentityProperty::CalendarAddress => {
|
||||
Value::Str(identity.calendar_address.to_string().into())
|
||||
}
|
||||
ParticipantIdentityProperty::IsDefault => {
|
||||
Value::Bool(identities.default == document_id)
|
||||
}
|
||||
};
|
||||
result.insert_unchecked(property.clone(), value);
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn participant_identity_get_or_create(
|
||||
&self,
|
||||
account_id: u32,
|
||||
) -> trc::Result<Option<Archive<AlignedBytes>>> {
|
||||
if let Some(identities) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
|
||||
account_id,
|
||||
Collection::Principal,
|
||||
0,
|
||||
PrincipalField::ParticipantIdentities,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(identities));
|
||||
}
|
||||
|
||||
// Obtain account info
|
||||
let account_info = self
|
||||
.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let name = account_info.description().unwrap_or(account_info.name());
|
||||
|
||||
// Build identities
|
||||
let identities = ParticipantIdentities {
|
||||
identities: account_info
|
||||
.addresses()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, email)| ParticipantIdentity {
|
||||
id: id as u32,
|
||||
name: None,
|
||||
calendar_address: format!("mailto:{email}"),
|
||||
})
|
||||
.collect(),
|
||||
default: 0,
|
||||
default_name: name.to_string(),
|
||||
};
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.set(
|
||||
PrincipalField::ParticipantIdentities,
|
||||
Archiver::new(identities)
|
||||
.serialize()
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
self.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
|
||||
account_id,
|
||||
Collection::Principal,
|
||||
0,
|
||||
PrincipalField::ParticipantIdentities,
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::participant_identity::get::ParticipantIdentityGet;
|
||||
use common::Server;
|
||||
use groupware::{
|
||||
calendar::{ParticipantIdentities, ParticipantIdentity},
|
||||
strip_mailto_scheme,
|
||||
};
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::participant_identity::{self, ParticipantIdentityProperty, ParticipantIdentityValue},
|
||||
request::{MaybeInvalid, reference::MaybeIdReference},
|
||||
};
|
||||
use jmap_tools::{Key, Value};
|
||||
use registry::schema::prelude::StorageQuota;
|
||||
use store::{
|
||||
Serialize,
|
||||
ahash::AHashSet,
|
||||
write::{Archiver, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::PrincipalField, id::Id};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub trait ParticipantIdentitySet: Sync + Send {
|
||||
fn participant_identity_set(
|
||||
&self,
|
||||
request: SetRequest<'_, participant_identity::ParticipantIdentity>,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<participant_identity::ParticipantIdentity>>> + Send;
|
||||
}
|
||||
|
||||
impl ParticipantIdentitySet for Server {
|
||||
async fn participant_identity_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, participant_identity::ParticipantIdentity>,
|
||||
) -> trc::Result<SetResponse<participant_identity::ParticipantIdentity>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
||||
let (identity_archive, mut identities) =
|
||||
match self.participant_identity_get_or_create(account_id).await? {
|
||||
Some(archive) => {
|
||||
let identities = archive
|
||||
.deserialize::<ParticipantIdentities>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
(Some(archive), identities)
|
||||
}
|
||||
None => (None, ParticipantIdentities::default()),
|
||||
};
|
||||
|
||||
let account_info = self
|
||||
.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Obtain allowed emails
|
||||
let allowed_emails = account_info
|
||||
.addresses()
|
||||
.iter()
|
||||
.map(|v| v.as_str())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
// Process creates
|
||||
let mut has_changes = false;
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
let mut identity = ParticipantIdentity::default();
|
||||
|
||||
if let Err(err) = validate_identity_value(None, object, &mut identity, &allowed_emails)
|
||||
{
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
if identities
|
||||
.identities
|
||||
.iter()
|
||||
.any(|i| i.calendar_address == identity.calendar_address)
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(ParticipantIdentityProperty::CalendarAddress)
|
||||
.with_description("Calendar address already in use.".to_string()),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if identities.identities.len()
|
||||
>= self.object_quota(
|
||||
account_info.object_quotas(),
|
||||
StorageQuota::MaxParticipantIdentities,
|
||||
) as usize
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::new(SetErrorType::OverQuota).with_description(concat!(
|
||||
"There are too many identities, ",
|
||||
"please delete some before adding a new one."
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
let document_id = identities
|
||||
.identities
|
||||
.iter()
|
||||
.map(|i| i.id)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
+ 1;
|
||||
identity.id = document_id;
|
||||
identities.identities.push(identity);
|
||||
|
||||
if let Some(MaybeIdReference::Reference(id_ref)) =
|
||||
&request.arguments.on_success_set_is_default
|
||||
&& id_ref == &id
|
||||
{
|
||||
identities.default = document_id;
|
||||
}
|
||||
|
||||
has_changes = true;
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
response.not_updated.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
let Some(identity) = identities
|
||||
.identities
|
||||
.iter_mut()
|
||||
.find(|i| i.id == id.document_id())
|
||||
else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
|
||||
if let Err(err) = validate_identity_value(Some(id), object, identity, &allowed_emails) {
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
has_changes = true;
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
for id in &will_destroy {
|
||||
let document_id = id.document_id();
|
||||
if identities.identities.iter().any(|i| i.id == document_id) {
|
||||
response.destroyed.push(*id);
|
||||
} else {
|
||||
response.not_destroyed.append(*id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
if !response.destroyed.is_empty() {
|
||||
has_changes = true;
|
||||
identities
|
||||
.identities
|
||||
.retain(|i| !response.destroyed.iter().any(|id| id.document_id() == i.id));
|
||||
}
|
||||
|
||||
if let Some(MaybeIdReference::Id(id)) = request.arguments.on_success_set_is_default {
|
||||
let id = id.document_id();
|
||||
if identities.identities.iter().any(|i| i.id == id) {
|
||||
identities.default = id;
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if has_changes {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0);
|
||||
if let Some(archive) = identity_archive {
|
||||
batch.assert_value(PrincipalField::ParticipantIdentities, archive);
|
||||
}
|
||||
batch.set(
|
||||
PrincipalField::ParticipantIdentities,
|
||||
Archiver::new(identities)
|
||||
.serialize()
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_identity_value(
|
||||
expected_id: Option<Id>,
|
||||
update: Value<'_, ParticipantIdentityProperty, ParticipantIdentityValue>,
|
||||
identity: &mut ParticipantIdentity,
|
||||
allowed_emails: &AHashSet<&str>,
|
||||
) -> Result<(), SetError<ParticipantIdentityProperty>> {
|
||||
for (property, value) in update.into_expanded_object() {
|
||||
let Key::Property(property) = property else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.to_owned())
|
||||
.with_description("Invalid property."));
|
||||
};
|
||||
|
||||
match (property, value) {
|
||||
(ParticipantIdentityProperty::Name, Value::Str(value)) if value.len() < 255 => {
|
||||
identity.name = value.into_owned().into();
|
||||
}
|
||||
(ParticipantIdentityProperty::CalendarAddress, Value::Str(value)) => {
|
||||
if identity.calendar_address != value {
|
||||
let email = sanitize_email(strip_mailto_scheme(&value));
|
||||
|
||||
if let Some(email) = email {
|
||||
if allowed_emails.iter().any(|e| e == &email) {
|
||||
identity.calendar_address = format!("mailto:{email}");
|
||||
} else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(ParticipantIdentityProperty::CalendarAddress)
|
||||
.with_description(
|
||||
"Calendar address not configured for this account.".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(ParticipantIdentityProperty::CalendarAddress)
|
||||
.with_description("Invalid or missing calendar address.".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
(ParticipantIdentityProperty::Id, value) => {
|
||||
if !expected_id.is_some_and(|expected| crate::matches_id(&value, expected)) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(ParticipantIdentityProperty::Id)
|
||||
.with_description("The id property is immutable."));
|
||||
}
|
||||
}
|
||||
(property, _) => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.clone())
|
||||
.with_description("Field could not be set."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email address
|
||||
if !identity.calendar_address.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SetError::invalid_properties()
|
||||
.with_property(ParticipantIdentityProperty::CalendarAddress)
|
||||
.with_description("Missing calendar address."))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user