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,278 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
object::{JmapRight, JmapSharedObject},
|
||||
};
|
||||
use jmap_tools::{JsonPointerIter, Key, Map, Property, Value};
|
||||
use registry::schema::prelude::ObjectType;
|
||||
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
id::Id,
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub struct JmapRights;
|
||||
|
||||
impl JmapRights {
|
||||
pub fn acl_set<T: JmapSharedObject>(
|
||||
value: Value<'_, T::Property, T::Element>,
|
||||
) -> Result<Vec<AclGrant>, SetError<T::Property>>
|
||||
where
|
||||
Id: TryFrom<T::Property>,
|
||||
T::Right: TryFrom<T::Property>,
|
||||
{
|
||||
let mut grants = Vec::new();
|
||||
|
||||
for (key, value) in value.into_expanded_object() {
|
||||
let account_id = key
|
||||
.try_into_property()
|
||||
.and_then(|p| Id::try_from(p).ok())
|
||||
.ok_or_else(|| {
|
||||
SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description("Invalid account id.")
|
||||
})?
|
||||
.document_id();
|
||||
|
||||
if !grants
|
||||
.iter()
|
||||
.any(|item: &AclGrant| item.account_id == account_id)
|
||||
{
|
||||
let acls = Self::map_acls::<T>(value)?;
|
||||
if !acls.is_empty() {
|
||||
grants.push(AclGrant {
|
||||
account_id,
|
||||
grants: acls,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(grants)
|
||||
}
|
||||
|
||||
pub fn acl_patch<T: JmapSharedObject>(
|
||||
mut grants: Vec<AclGrant>,
|
||||
mut path: JsonPointerIter<'_, T::Property>,
|
||||
value: Value<'_, T::Property, T::Element>,
|
||||
) -> Result<Vec<AclGrant>, SetError<T::Property>>
|
||||
where
|
||||
Id: TryFrom<T::Property>,
|
||||
T::Right: TryFrom<T::Property>,
|
||||
{
|
||||
let account_id = path
|
||||
.next()
|
||||
.and_then(|item| item.as_property_key())
|
||||
.cloned()
|
||||
.and_then(|p| Id::try_from(p).ok())
|
||||
.ok_or_else(|| {
|
||||
SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description("Invalid account id.")
|
||||
})?
|
||||
.document_id();
|
||||
|
||||
if let Some(right) = path.next() {
|
||||
if path.next().is_some() {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description("Invalid path for ACL patch."));
|
||||
}
|
||||
|
||||
let is_set = match value {
|
||||
Value::Bool(is_set) => is_set,
|
||||
Value::Null => false,
|
||||
_ => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description("Invalid ACL value."));
|
||||
}
|
||||
};
|
||||
|
||||
let acl = right
|
||||
.as_property_key()
|
||||
.cloned()
|
||||
.and_then(|p| T::Right::try_from(p).ok())
|
||||
.ok_or_else(|| {
|
||||
SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description(format!(
|
||||
"Invalid permission {:?}.",
|
||||
right.to_cow().unwrap_or_default()
|
||||
))
|
||||
})?
|
||||
.to_acl()
|
||||
.iter()
|
||||
.copied();
|
||||
|
||||
if let Some(acl_item) = grants.iter_mut().find(|item| item.account_id == account_id) {
|
||||
if is_set {
|
||||
acl_item.grants.insert_many(acl);
|
||||
} else {
|
||||
acl_item.grants.remove_many(acl);
|
||||
if acl_item.grants.is_empty() {
|
||||
grants.retain(|item| item.account_id != account_id);
|
||||
}
|
||||
}
|
||||
} else if is_set {
|
||||
grants.push(AclGrant {
|
||||
account_id,
|
||||
grants: Bitmap::from_iter(acl),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let acls = Self::map_acls::<T>(value)?;
|
||||
if !acls.is_empty() {
|
||||
if let Some(acl_item) = grants.iter_mut().find(|item| item.account_id == account_id)
|
||||
{
|
||||
acl_item.grants = acls;
|
||||
} else {
|
||||
grants.push(AclGrant {
|
||||
account_id,
|
||||
grants: acls,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
grants.retain(|item| item.account_id != account_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(grants)
|
||||
}
|
||||
|
||||
fn map_acls<T: JmapSharedObject>(
|
||||
value: Value<'_, T::Property, T::Element>,
|
||||
) -> Result<Bitmap<Acl>, SetError<T::Property>>
|
||||
where
|
||||
Id: TryFrom<T::Property>,
|
||||
T::Right: TryFrom<T::Property>,
|
||||
{
|
||||
let mut acls = Bitmap::new();
|
||||
|
||||
for key in value.into_expanded_boolean_set() {
|
||||
acls.insert_many(
|
||||
key.as_property()
|
||||
.and_then(|p| T::Right::try_from(p.clone()).ok())
|
||||
.ok_or_else(|| {
|
||||
SetError::invalid_properties()
|
||||
.with_property(T::SHARE_WITH_PROPERTY)
|
||||
.with_description(format!("Invalid permission {:?}.", key.to_string()))
|
||||
})?
|
||||
.to_acl()
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(acls)
|
||||
}
|
||||
|
||||
pub fn all_rights<T: JmapSharedObject>() -> Value<'static, T::Property, T::Element> {
|
||||
let rights = T::Right::all_rights();
|
||||
let mut obj = Map::with_capacity(rights.len());
|
||||
|
||||
for right in rights {
|
||||
obj.insert_unchecked(Key::Property((*right).into()), Value::Bool(true));
|
||||
}
|
||||
|
||||
Value::Object(obj)
|
||||
}
|
||||
|
||||
pub fn rights<T: JmapSharedObject>(
|
||||
acls: Bitmap<Acl>,
|
||||
) -> Value<'static, T::Property, T::Element> {
|
||||
let mut obj = Map::with_capacity(3);
|
||||
|
||||
for right in T::Right::all_rights() {
|
||||
obj.insert_unchecked(
|
||||
Key::Property((*right).into()),
|
||||
Value::Bool(right.to_acl().iter().all(|acl| acls.contains(*acl))),
|
||||
);
|
||||
}
|
||||
|
||||
Value::Object(obj)
|
||||
}
|
||||
|
||||
pub fn share_with<T: JmapSharedObject>(
|
||||
account_id: u32,
|
||||
access_token: &AccessToken,
|
||||
grants: &[AclGrant],
|
||||
) -> Value<'static, T::Property, T::Element>
|
||||
where
|
||||
T::Property: From<Id>,
|
||||
{
|
||||
if access_token.is_member(account_id)
|
||||
|| grants.effective_acl(access_token).contains(Acl::Share)
|
||||
{
|
||||
let mut share_with = Map::with_capacity(grants.len());
|
||||
for grant in grants {
|
||||
share_with.insert_unchecked(
|
||||
Key::Property(Id::from(grant.account_id).into()),
|
||||
Self::rights::<T>(grant.grants),
|
||||
);
|
||||
}
|
||||
|
||||
Value::Object(share_with)
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait JmapAcl {
|
||||
fn acl_validate(
|
||||
&self,
|
||||
grants: &[AclGrant],
|
||||
) -> impl Future<Output = Result<(), ShareValidationError>> + Send;
|
||||
}
|
||||
|
||||
pub enum ShareValidationError {
|
||||
MaxSharesExceeded(usize),
|
||||
InvalidAccountId(Id),
|
||||
}
|
||||
|
||||
impl JmapAcl for Server {
|
||||
async fn acl_validate(&self, grants: &[AclGrant]) -> Result<(), ShareValidationError> {
|
||||
if grants.len() > self.core.groupware.max_shares_per_item {
|
||||
return Err(ShareValidationError::MaxSharesExceeded(
|
||||
self.core.groupware.max_shares_per_item,
|
||||
));
|
||||
}
|
||||
|
||||
let principal_ids = self
|
||||
.registry()
|
||||
.query::<RoaringBitmap>(RegistryQuery::new(ObjectType::Account))
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for grant in grants {
|
||||
if !principal_ids.contains(grant.account_id) {
|
||||
return Err(ShareValidationError::InvalidAccountId(Id::from(
|
||||
grant.account_id,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> From<ShareValidationError> for SetError<T> {
|
||||
fn from(err: ShareValidationError) -> Self {
|
||||
match err {
|
||||
ShareValidationError::MaxSharesExceeded(max) => SetError::invalid_properties()
|
||||
.with_description(format!(
|
||||
"Maximum number of shares per item exceeded (max: {max})"
|
||||
)),
|
||||
ShareValidationError::InvalidAccountId(id) => SetError::invalid_properties()
|
||||
.with_description(format!("Account id {id} is invalid.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
method::set::SetRequest,
|
||||
object::JmapObject,
|
||||
request::{
|
||||
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
|
||||
QueryRequestMethod, RequestMethod, SetRequestMethod, method::MethodObject,
|
||||
reference::MaybeResultReference,
|
||||
},
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use types::{collection::Collection, id::Id};
|
||||
|
||||
pub trait JmapAuthorization {
|
||||
fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self>;
|
||||
fn assert_has_jmap_permission(
|
||||
&self,
|
||||
request: &RequestMethod,
|
||||
object: MethodObject,
|
||||
) -> trc::Result<()>;
|
||||
fn assert_has_access(&self, to_account_id: Id, to_collection: Collection)
|
||||
-> trc::Result<&Self>;
|
||||
}
|
||||
|
||||
impl JmapAuthorization for AccessToken {
|
||||
fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> {
|
||||
if self.is_member(account_id.document_id()) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details(format!("You are not an owner of account {}", account_id)))
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_has_access(
|
||||
&self,
|
||||
to_account_id: Id,
|
||||
to_collection: Collection,
|
||||
) -> trc::Result<&Self> {
|
||||
if self.has_access(to_account_id.document_id(), to_collection) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden.into_err().details(format!(
|
||||
"You do not have access to account {}",
|
||||
to_account_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_has_jmap_permission(
|
||||
&self,
|
||||
request: &RequestMethod,
|
||||
object: MethodObject,
|
||||
) -> trc::Result<()> {
|
||||
let permission = match request {
|
||||
RequestMethod::Get(m) => match &m {
|
||||
GetRequestMethod::Email(_) => Permission::JmapEmailGet,
|
||||
GetRequestMethod::Mailbox(_) => Permission::JmapMailboxGet,
|
||||
GetRequestMethod::Thread(_) => Permission::JmapThreadGet,
|
||||
GetRequestMethod::Identity(_) => Permission::JmapIdentityGet,
|
||||
GetRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionGet,
|
||||
GetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionGet,
|
||||
GetRequestMethod::Sieve(_) => Permission::JmapSieveScriptGet,
|
||||
GetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseGet,
|
||||
GetRequestMethod::Principal(_) => Permission::JmapPrincipalGet,
|
||||
GetRequestMethod::Quota(_) => Permission::JmapQuotaGet,
|
||||
GetRequestMethod::Blob(_) => Permission::JmapBlobGet,
|
||||
GetRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet,
|
||||
GetRequestMethod::ContactCard(_) => Permission::JmapContactCardGet,
|
||||
GetRequestMethod::FileNode(_) => Permission::JmapFileNodeGet,
|
||||
GetRequestMethod::PrincipalAvailability(_) => {
|
||||
Permission::JmapPrincipalGetAvailability
|
||||
}
|
||||
GetRequestMethod::Calendar(_) => Permission::JmapCalendarGet,
|
||||
GetRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventGet,
|
||||
GetRequestMethod::CalendarEventNotification(_) => {
|
||||
Permission::JmapCalendarEventNotificationGet
|
||||
}
|
||||
GetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentityGet,
|
||||
GetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationGet,
|
||||
GetRequestMethod::Registry(_) => {
|
||||
let MethodObject::Registry(object_type) = object else {
|
||||
unreachable!()
|
||||
};
|
||||
object_type.get_permission()
|
||||
}
|
||||
},
|
||||
RequestMethod::Set(m) => {
|
||||
return match &m {
|
||||
SetRequestMethod::Email(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapEmailCreate,
|
||||
Permission::JmapEmailUpdate,
|
||||
Permission::JmapEmailDestroy,
|
||||
),
|
||||
SetRequestMethod::Mailbox(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapMailboxCreate,
|
||||
Permission::JmapMailboxUpdate,
|
||||
Permission::JmapMailboxDestroy,
|
||||
),
|
||||
SetRequestMethod::Identity(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapIdentityCreate,
|
||||
Permission::JmapIdentityUpdate,
|
||||
Permission::JmapIdentityDestroy,
|
||||
),
|
||||
SetRequestMethod::EmailSubmission(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapEmailSubmissionCreate,
|
||||
Permission::JmapEmailSubmissionUpdate,
|
||||
Permission::JmapEmailSubmissionDestroy,
|
||||
),
|
||||
SetRequestMethod::PushSubscription(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapPushSubscriptionCreate,
|
||||
Permission::JmapPushSubscriptionUpdate,
|
||||
Permission::JmapPushSubscriptionDestroy,
|
||||
),
|
||||
SetRequestMethod::Sieve(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapSieveScriptCreate,
|
||||
Permission::JmapSieveScriptUpdate,
|
||||
Permission::JmapSieveScriptDestroy,
|
||||
),
|
||||
SetRequestMethod::VacationResponse(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapVacationResponseCreate,
|
||||
Permission::JmapVacationResponseUpdate,
|
||||
Permission::JmapVacationResponseDestroy,
|
||||
),
|
||||
SetRequestMethod::AddressBook(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapAddressBookCreate,
|
||||
Permission::JmapAddressBookUpdate,
|
||||
Permission::JmapAddressBookDestroy,
|
||||
),
|
||||
SetRequestMethod::ContactCard(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapContactCardCreate,
|
||||
Permission::JmapContactCardUpdate,
|
||||
Permission::JmapContactCardDestroy,
|
||||
),
|
||||
SetRequestMethod::FileNode(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapFileNodeCreate,
|
||||
Permission::JmapFileNodeUpdate,
|
||||
Permission::JmapFileNodeDestroy,
|
||||
),
|
||||
SetRequestMethod::ShareNotification(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapShareNotificationCreate,
|
||||
Permission::JmapShareNotificationUpdate,
|
||||
Permission::JmapShareNotificationDestroy,
|
||||
),
|
||||
SetRequestMethod::Calendar(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapCalendarCreate,
|
||||
Permission::JmapCalendarUpdate,
|
||||
Permission::JmapCalendarDestroy,
|
||||
),
|
||||
SetRequestMethod::CalendarEvent(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapCalendarEventCreate,
|
||||
Permission::JmapCalendarEventUpdate,
|
||||
Permission::JmapCalendarEventDestroy,
|
||||
),
|
||||
SetRequestMethod::CalendarEventNotification(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapCalendarEventNotificationCreate,
|
||||
Permission::JmapCalendarEventNotificationUpdate,
|
||||
Permission::JmapCalendarEventNotificationDestroy,
|
||||
),
|
||||
SetRequestMethod::ParticipantIdentity(s) => validate_set(
|
||||
s,
|
||||
self,
|
||||
Permission::JmapParticipantIdentityCreate,
|
||||
Permission::JmapParticipantIdentityUpdate,
|
||||
Permission::JmapParticipantIdentityDestroy,
|
||||
),
|
||||
SetRequestMethod::Registry(s) => {
|
||||
let MethodObject::Registry(object_type) = object else {
|
||||
unreachable!()
|
||||
};
|
||||
let set_permissions = object_type.set_permission();
|
||||
validate_set(
|
||||
s,
|
||||
self,
|
||||
set_permissions[0],
|
||||
set_permissions[1],
|
||||
set_permissions[2],
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
RequestMethod::Changes(_) => match object {
|
||||
MethodObject::Email => Permission::JmapEmailChanges,
|
||||
MethodObject::Mailbox => Permission::JmapMailboxChanges,
|
||||
MethodObject::Thread => Permission::JmapThreadChanges,
|
||||
MethodObject::Identity => Permission::JmapIdentityChanges,
|
||||
MethodObject::EmailSubmission => Permission::JmapEmailSubmissionChanges,
|
||||
MethodObject::Quota => Permission::JmapQuotaChanges,
|
||||
MethodObject::ContactCard => Permission::JmapContactCardChanges,
|
||||
MethodObject::FileNode => Permission::JmapFileNodeChanges,
|
||||
MethodObject::Calendar => Permission::JmapCalendarChanges,
|
||||
MethodObject::CalendarEvent => Permission::JmapCalendarEventChanges,
|
||||
MethodObject::CalendarEventNotification => {
|
||||
Permission::JmapCalendarEventNotificationChanges
|
||||
}
|
||||
MethodObject::ParticipantIdentity => Permission::JmapParticipantIdentityChanges,
|
||||
MethodObject::ShareNotification => Permission::JmapShareNotificationChanges,
|
||||
MethodObject::Principal => Permission::JmapPrincipalChanges,
|
||||
MethodObject::AddressBook => Permission::JmapAddressBookChanges,
|
||||
MethodObject::Core
|
||||
| MethodObject::Blob
|
||||
| MethodObject::PushSubscription
|
||||
| MethodObject::SearchSnippet
|
||||
| MethodObject::VacationResponse
|
||||
| MethodObject::SieveScript
|
||||
| MethodObject::Registry(_) => Permission::JmapEmailChanges,
|
||||
},
|
||||
RequestMethod::Copy(m) => match &m {
|
||||
CopyRequestMethod::Email(_) => Permission::JmapEmailCopy,
|
||||
CopyRequestMethod::Blob(_) => Permission::JmapBlobCopy,
|
||||
CopyRequestMethod::ContactCard(_) => Permission::JmapContactCardCopy,
|
||||
CopyRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventCopy,
|
||||
CopyRequestMethod::FileNode(_) => Permission::JmapFileNodeCopy,
|
||||
},
|
||||
RequestMethod::ImportEmail(_) => Permission::JmapEmailImport,
|
||||
RequestMethod::Parse(m) => match &m {
|
||||
ParseRequestMethod::Email(_) => Permission::JmapEmailParse,
|
||||
ParseRequestMethod::ContactCard(_) => Permission::JmapContactCardParse,
|
||||
ParseRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventParse,
|
||||
},
|
||||
RequestMethod::QueryChanges(m) => match m {
|
||||
QueryChangesRequestMethod::Email(_) => Permission::JmapEmailQueryChanges,
|
||||
QueryChangesRequestMethod::Mailbox(_) => Permission::JmapMailboxQueryChanges,
|
||||
QueryChangesRequestMethod::EmailSubmission(_) => {
|
||||
Permission::JmapEmailSubmissionQueryChanges
|
||||
}
|
||||
QueryChangesRequestMethod::Principal(_) => Permission::JmapPrincipalQueryChanges,
|
||||
QueryChangesRequestMethod::Quota(_) => Permission::JmapQuotaQueryChanges,
|
||||
QueryChangesRequestMethod::ContactCard(_) => {
|
||||
Permission::JmapContactCardQueryChanges
|
||||
}
|
||||
QueryChangesRequestMethod::FileNode(_) => Permission::JmapFileNodeQueryChanges,
|
||||
QueryChangesRequestMethod::CalendarEvent(_) => {
|
||||
Permission::JmapCalendarEventQueryChanges
|
||||
}
|
||||
QueryChangesRequestMethod::CalendarEventNotification(_) => {
|
||||
Permission::JmapCalendarEventNotificationQueryChanges
|
||||
}
|
||||
QueryChangesRequestMethod::ShareNotification(_) => {
|
||||
Permission::JmapShareNotificationQueryChanges
|
||||
}
|
||||
},
|
||||
RequestMethod::Query(m) => match m {
|
||||
QueryRequestMethod::Email(_) => Permission::JmapEmailQuery,
|
||||
QueryRequestMethod::Mailbox(_) => Permission::JmapMailboxQuery,
|
||||
QueryRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionQuery,
|
||||
QueryRequestMethod::Sieve(_) => Permission::JmapSieveScriptQuery,
|
||||
QueryRequestMethod::Principal(_) => Permission::JmapPrincipalQuery,
|
||||
QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery,
|
||||
QueryRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet,
|
||||
QueryRequestMethod::ContactCard(_) => Permission::JmapContactCardQuery,
|
||||
QueryRequestMethod::FileNode(_) => Permission::JmapFileNodeQuery,
|
||||
QueryRequestMethod::Calendar(_) => Permission::JmapCalendarGet,
|
||||
QueryRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventQuery,
|
||||
QueryRequestMethod::CalendarEventNotification(_) => {
|
||||
Permission::JmapCalendarEventNotificationQuery
|
||||
}
|
||||
QueryRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationQuery,
|
||||
QueryRequestMethod::Registry(_) => {
|
||||
let MethodObject::Registry(object_type) = object else {
|
||||
unreachable!()
|
||||
};
|
||||
object_type.query_permission()
|
||||
}
|
||||
},
|
||||
RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippetGet,
|
||||
RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate,
|
||||
RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup,
|
||||
RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload,
|
||||
RequestMethod::Echo(_) => Permission::JmapCoreEcho,
|
||||
RequestMethod::Error(_) => return Ok(()),
|
||||
};
|
||||
|
||||
if self.has_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to perform this action"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_set<T: JmapObject>(
|
||||
set: &SetRequest<'_, T>,
|
||||
access_token: &AccessToken,
|
||||
create_permission: Permission,
|
||||
update_permission: Permission,
|
||||
destroy_permission: Permission,
|
||||
) -> trc::Result<()> {
|
||||
let can_create = access_token.has_permission(create_permission);
|
||||
let can_update = access_token.has_permission(update_permission);
|
||||
let can_destroy = access_token.has_permission(destroy_permission);
|
||||
|
||||
if can_create && can_update && can_destroy {
|
||||
Ok(())
|
||||
} else if !can_create && !can_update && !can_destroy {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to create, update or destroy objects of this type"))
|
||||
} else if !can_create && set.create.as_ref().is_some_and(|objs| !objs.is_empty()) {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to create objects of this type"))
|
||||
} else if !can_update && set.update.as_ref().is_some_and(|objs| !objs.is_empty()) {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to update objects of this type"))
|
||||
} else if !can_destroy
|
||||
&& set.destroy.as_ref().is_some_and(|objs| match objs {
|
||||
MaybeResultReference::Value(v) => !v.is_empty(),
|
||||
MaybeResultReference::Reference(_) => true,
|
||||
})
|
||||
{
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to destroy objects of this type"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::api::IntoPushObject;
|
||||
use common::{LONG_1D_SLUMBER, Server, auth::AccessToken, ipc::PushNotification};
|
||||
use http_body_util::{StreamBody, combinators::BoxBody};
|
||||
use http_proto::*;
|
||||
use hyper::{
|
||||
StatusCode,
|
||||
body::{Bytes, Frame},
|
||||
};
|
||||
use jmap_proto::{response::status::PushObject, types::state::State};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{future::Future, str::FromStr};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::map::{bitmap::Bitmap, vec_map::VecMap};
|
||||
|
||||
struct Ping {
|
||||
interval: Duration,
|
||||
last_ping: Instant,
|
||||
payload: Bytes,
|
||||
}
|
||||
|
||||
pub trait EventSourceHandler: Sync + Send {
|
||||
fn handle_event_source(
|
||||
&self,
|
||||
req: HttpRequest,
|
||||
access_token: AccessToken,
|
||||
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl EventSourceHandler for Server {
|
||||
async fn handle_event_source(
|
||||
&self,
|
||||
req: HttpRequest,
|
||||
access_token: AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Parse query
|
||||
let mut ping = 0;
|
||||
let mut types = Bitmap::default();
|
||||
let mut close_after_state = false;
|
||||
|
||||
for (key, value) in
|
||||
http_proto::form_urlencoded::parse(req.uri().query().unwrap_or_default().as_bytes())
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
"types" => {
|
||||
for type_state in value.split(',') {
|
||||
if type_state == "*" {
|
||||
types = Bitmap::all();
|
||||
break;
|
||||
} else if let Ok(type_state) = DataType::from_str(type_state) {
|
||||
types.insert(type_state);
|
||||
} else {
|
||||
return Err(trc::ResourceEvent::BadParameters.into_err());
|
||||
}
|
||||
}
|
||||
},
|
||||
"closeafter" => match value.as_ref() {
|
||||
"state" => {
|
||||
close_after_state = true;
|
||||
}
|
||||
"no" => {}
|
||||
_ => return Err(trc::ResourceEvent::BadParameters.into_err()),
|
||||
},
|
||||
"ping" => match value.parse::<u32>() {
|
||||
Ok(value) => {
|
||||
ping = value;
|
||||
}
|
||||
Err(_) => return Err(trc::ResourceEvent::BadParameters.into_err()),
|
||||
},
|
||||
_ => {}
|
||||
);
|
||||
}
|
||||
|
||||
let mut ping = if ping > 0 {
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
let interval = std::cmp::max(ping, 30);
|
||||
#[cfg(feature = "test_mode")]
|
||||
let interval = ping;
|
||||
let interval_duration = Duration::from_secs(interval as u64);
|
||||
|
||||
Ping {
|
||||
interval: interval_duration,
|
||||
last_ping: Instant::now() - interval_duration,
|
||||
payload: Bytes::from(format!(
|
||||
"event: ping\ndata: {{\"interval\": {}}}\n\n",
|
||||
interval
|
||||
)),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Register with push manager
|
||||
let mut push_rx = self.subscribe_push_manager(&access_token, types).await?;
|
||||
let mut changed: VecMap<Id, VecMap<DataType, State>> = VecMap::new();
|
||||
let throttle = self.core.jmap.event_source_throttle;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/event-stream")
|
||||
.with_cache_control("no-store")
|
||||
.with_stream_body(BoxBody::new(StreamBody::new(async_stream::stream! {
|
||||
let mut last_message = Instant::now() - throttle;
|
||||
let mut timeout =
|
||||
ping.as_ref().map(|p| p.interval).unwrap_or(LONG_1D_SLUMBER);
|
||||
|
||||
loop {
|
||||
match tokio::time::timeout(timeout, push_rx.recv()).await {
|
||||
Ok(Some(notification)) => {
|
||||
match notification {
|
||||
PushNotification::StateChange(state_change) => {
|
||||
for type_state in state_change.types {
|
||||
changed
|
||||
.get_mut_or_insert(state_change.account_id.into())
|
||||
.set(type_state, State::Exact(state_change.change_id));
|
||||
}
|
||||
}
|
||||
PushNotification::CalendarAlert(calendar_alert) => {
|
||||
yield Ok(Frame::data(Bytes::from(format!(
|
||||
"event: calendarAlert\ndata: {}\n\n",
|
||||
serde_json::to_string(&calendar_alert.into_push_object()).unwrap()
|
||||
))));
|
||||
}
|
||||
PushNotification::EmailPush(email_push) => {
|
||||
let state_change = email_push.to_state_change();
|
||||
for type_state in state_change.types {
|
||||
changed
|
||||
.get_mut_or_insert(state_change.account_id.into())
|
||||
.set(type_state, State::Exact(state_change.change_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
timeout = if !changed.is_empty() {
|
||||
let elapsed = last_message.elapsed();
|
||||
if elapsed >= throttle {
|
||||
last_message = Instant::now();
|
||||
let response =
|
||||
PushObject::StateChange { changed: std::mem::take(&mut changed) };
|
||||
|
||||
yield Ok(Frame::data(Bytes::from(format!(
|
||||
"event: state\ndata: {}\n\n",
|
||||
serde_json::to_string(&response).unwrap()
|
||||
))));
|
||||
|
||||
if close_after_state {
|
||||
break;
|
||||
}
|
||||
|
||||
ping.as_ref().map(|p| p.interval).unwrap_or(LONG_1D_SLUMBER)
|
||||
} else {
|
||||
throttle - elapsed
|
||||
}
|
||||
} else if let Some(ping) = &mut ping {
|
||||
let elapsed = ping.last_ping.elapsed();
|
||||
if elapsed >= ping.interval {
|
||||
ping.last_ping = Instant::now();
|
||||
yield Ok(Frame::data(ping.payload.clone()));
|
||||
ping.interval
|
||||
} else {
|
||||
ping.interval - elapsed
|
||||
}
|
||||
} else {
|
||||
LONG_1D_SLUMBER
|
||||
};
|
||||
}
|
||||
}))))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::blob::UploadResponse;
|
||||
use calcard::jscalendar::JSCalendarDateTime;
|
||||
use common::ipc::{CalendarAlert, PushNotification};
|
||||
use http_proto::{HttpResponse, JsonResponse, ToHttpResponse};
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::{
|
||||
error::request::{RateLimitPolicy, RateLimitUnit, RequestError, RequestLimitError},
|
||||
request::capability::Session,
|
||||
response::{Response, status::PushObject},
|
||||
types::state::State,
|
||||
};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub mod acl;
|
||||
pub mod auth;
|
||||
pub mod event_source;
|
||||
pub mod query;
|
||||
pub mod request;
|
||||
pub mod session;
|
||||
|
||||
impl ToHttpResponse for UploadResponse {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
JsonResponse::new(self).into_http_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToJmapHttpResponse {
|
||||
fn into_http_response(self) -> HttpResponse;
|
||||
}
|
||||
|
||||
impl ToJmapHttpResponse for Response<'_> {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
JsonResponse::new(self).into_http_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToJmapHttpResponse for Session {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
JsonResponse::new(self).into_http_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToJmapHttpResponse for RequestError<'_> {
|
||||
fn into_http_response(self) -> HttpResponse {
|
||||
let mut response =
|
||||
HttpResponse::new(StatusCode::from_u16(self.status).unwrap_or(StatusCode::BAD_REQUEST));
|
||||
if let Some(retry_after) = self.retry_after {
|
||||
response = response.with_header("Retry-After", retry_after.to_string());
|
||||
}
|
||||
if let Some(policy) = self.rate_limit_policy_header() {
|
||||
response = response.with_header("RateLimit-Policy", policy);
|
||||
}
|
||||
if let Some(state) = self.rate_limit_state_header() {
|
||||
response = response.with_header("RateLimit", state);
|
||||
}
|
||||
response
|
||||
.with_content_type("application/problem+json")
|
||||
.with_text_body(serde_json::to_string(&self).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToRequestError {
|
||||
fn to_request_error(&self) -> RequestError<'_>;
|
||||
}
|
||||
|
||||
impl ToRequestError for trc::Error {
|
||||
fn to_request_error(&self) -> RequestError<'_> {
|
||||
let details_or_reason = self
|
||||
.value(trc::Key::Details)
|
||||
.or_else(|| self.value(trc::Key::Reason))
|
||||
.and_then(|v| v.as_str());
|
||||
let details = details_or_reason.unwrap_or_else(|| self.as_ref().message());
|
||||
|
||||
match self.as_ref() {
|
||||
trc::EventType::Jmap(cause) => match cause {
|
||||
trc::JmapEvent::UnknownCapability => RequestError::unknown_capability(details),
|
||||
trc::JmapEvent::NotJson => RequestError::not_json(details),
|
||||
trc::JmapEvent::NotRequest => RequestError::not_request(details),
|
||||
_ => RequestError::invalid_parameters(),
|
||||
},
|
||||
trc::EventType::Limit(cause) => {
|
||||
let reset = self.value(trc::Key::Expires).and_then(|v| v.to_uint());
|
||||
let limit = self.value(trc::Key::Limit).and_then(|v| v.to_uint());
|
||||
let total = self.value(trc::Key::Total).and_then(|v| v.to_uint());
|
||||
let size = self.value(trc::Key::Size).and_then(|v| v.to_uint());
|
||||
|
||||
match cause {
|
||||
trc::LimitEvent::SizeRequest => {
|
||||
RequestError::limit(RequestLimitError::SizeRequest)
|
||||
}
|
||||
trc::LimitEvent::SizeUpload => {
|
||||
RequestError::limit(RequestLimitError::SizeUpload)
|
||||
}
|
||||
trc::LimitEvent::CallsIn => RequestError::limit(RequestLimitError::CallsIn),
|
||||
trc::LimitEvent::ConcurrentRequest | trc::LimitEvent::ConcurrentConnection => {
|
||||
let mut policy =
|
||||
RateLimitPolicy::new("concurrent-requests", limit.unwrap_or(0))
|
||||
.with_unit(RateLimitUnit::ConcurrentRequests);
|
||||
if let Some(reset) = reset {
|
||||
policy = policy.with_reset(reset);
|
||||
}
|
||||
RequestError::limit(RequestLimitError::ConcurrentRequest)
|
||||
.with_rate_limit(policy)
|
||||
}
|
||||
trc::LimitEvent::ConcurrentUpload => {
|
||||
let mut policy =
|
||||
RateLimitPolicy::new("concurrent-uploads", limit.unwrap_or(0))
|
||||
.with_unit(RateLimitUnit::ConcurrentRequests);
|
||||
if let Some(reset) = reset {
|
||||
policy = policy.with_reset(reset);
|
||||
}
|
||||
RequestError::limit(RequestLimitError::ConcurrentUpload)
|
||||
.with_rate_limit(policy)
|
||||
}
|
||||
trc::LimitEvent::Quota => RequestError::over_quota(),
|
||||
trc::LimitEvent::TenantQuota => RequestError::tenant_over_quota(),
|
||||
trc::LimitEvent::BlobQuota => {
|
||||
let mut err = RequestError::over_blob_quota(
|
||||
total.unwrap_or(0) as usize,
|
||||
size.unwrap_or(0) as usize,
|
||||
);
|
||||
if let Some(total) = total {
|
||||
let mut policy = RateLimitPolicy::new("blob-upload-files", total);
|
||||
if let Some(reset) = reset {
|
||||
policy = policy.with_reset(reset);
|
||||
}
|
||||
err = err.with_rate_limit(policy);
|
||||
}
|
||||
if let Some(size) = size {
|
||||
let mut policy = RateLimitPolicy::new("blob-upload-bytes", size)
|
||||
.with_unit(RateLimitUnit::ContentBytes);
|
||||
if let Some(reset) = reset {
|
||||
policy = policy.with_reset(reset);
|
||||
}
|
||||
err = err.with_rate_limit(policy);
|
||||
}
|
||||
err
|
||||
}
|
||||
trc::LimitEvent::TooManyRequests => {
|
||||
let mut err = RequestError::too_many_requests();
|
||||
if let Some(limit) = limit {
|
||||
let mut policy = RateLimitPolicy::new("requests", limit);
|
||||
if let Some(reset) = reset {
|
||||
policy = policy.with_reset(reset);
|
||||
}
|
||||
err = err.with_rate_limit(policy);
|
||||
} else if let Some(reset) = reset {
|
||||
err = err.with_retry_after(reset);
|
||||
}
|
||||
err
|
||||
}
|
||||
}
|
||||
}
|
||||
trc::EventType::Auth(cause) => match cause {
|
||||
trc::AuthEvent::MfaRequired => {
|
||||
RequestError::blank(402, "MFA code required", self.as_ref().message())
|
||||
}
|
||||
trc::AuthEvent::TooManyAttempts => {
|
||||
let mut err = RequestError::too_many_auth_attempts();
|
||||
if let Some(reset) = self.value(trc::Key::Expires).and_then(|v| v.to_uint()) {
|
||||
err = err.with_retry_after(reset);
|
||||
}
|
||||
err
|
||||
}
|
||||
_ => RequestError::unauthorized(),
|
||||
},
|
||||
trc::EventType::Security(cause) => match cause {
|
||||
trc::SecurityEvent::AuthenticationBan
|
||||
| trc::SecurityEvent::ScanBan
|
||||
| trc::SecurityEvent::AbuseBan
|
||||
| trc::SecurityEvent::LoiterBan
|
||||
| trc::SecurityEvent::IpBlocked => {
|
||||
let mut err = RequestError::too_many_auth_attempts();
|
||||
if let Some(reset) = self.value(trc::Key::Expires).and_then(|v| v.to_uint()) {
|
||||
err = err.with_retry_after(reset);
|
||||
}
|
||||
err
|
||||
}
|
||||
trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => {
|
||||
RequestError::forbidden()
|
||||
}
|
||||
trc::SecurityEvent::IpBlockExpired | trc::SecurityEvent::IpAllowExpired => {
|
||||
RequestError::internal_server_error()
|
||||
}
|
||||
},
|
||||
trc::EventType::Resource(cause) => match cause {
|
||||
trc::ResourceEvent::NotFound => RequestError::not_found(),
|
||||
trc::ResourceEvent::BadParameters => RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
details_or_reason.unwrap_or("One or multiple parameters could not be parsed."),
|
||||
),
|
||||
trc::ResourceEvent::Error => RequestError::internal_server_error(),
|
||||
_ => RequestError::internal_server_error(),
|
||||
},
|
||||
_ => RequestError::internal_server_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait IntoPushObject {
|
||||
fn into_push_object(self) -> PushObject;
|
||||
}
|
||||
|
||||
pub(crate) fn notifications_into_push_objects(
|
||||
notifications: Vec<PushNotification>,
|
||||
) -> Vec<PushObject> {
|
||||
let mut changed: VecMap<Id, VecMap<DataType, State>> = VecMap::new();
|
||||
let mut objects = Vec::with_capacity(notifications.len());
|
||||
for notification in notifications {
|
||||
match notification {
|
||||
PushNotification::StateChange(state_change) => {
|
||||
for type_state in state_change.types {
|
||||
changed
|
||||
.get_mut_or_insert(state_change.account_id.into())
|
||||
.set(type_state, State::Exact(state_change.change_id));
|
||||
}
|
||||
}
|
||||
PushNotification::CalendarAlert(calendar_alert) => {
|
||||
objects.push(calendar_alert.into_push_object());
|
||||
}
|
||||
PushNotification::EmailPush(email_push) => {
|
||||
let state_change = email_push.to_state_change();
|
||||
for type_state in state_change.types {
|
||||
changed
|
||||
.get_mut_or_insert(state_change.account_id.into())
|
||||
.set(type_state, State::Exact(state_change.change_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed.is_empty() {
|
||||
objects.push(PushObject::StateChange { changed });
|
||||
}
|
||||
objects
|
||||
}
|
||||
|
||||
impl IntoPushObject for CalendarAlert {
|
||||
fn into_push_object(self) -> PushObject {
|
||||
PushObject::CalendarAlert {
|
||||
account_id: self.account_id.into(),
|
||||
calendar_event_id: self.event_id.into(),
|
||||
uid: self.uid,
|
||||
recurrence_id: self
|
||||
.recurrence_id
|
||||
.map(|timestamp| JSCalendarDateTime::new(timestamp, true).to_rfc3339()),
|
||||
alert_id: self.alert_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_proto::{
|
||||
method::query::{QueryRequest, QueryResponse},
|
||||
object::JmapObject,
|
||||
types::state::State,
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
pub struct QueryResponseBuilder {
|
||||
requested_position: i32,
|
||||
position: i32,
|
||||
pub limit: usize,
|
||||
anchor: u64,
|
||||
anchor_offset: i32,
|
||||
pub has_anchor: bool,
|
||||
pub anchor_found: bool,
|
||||
index: i32,
|
||||
|
||||
pub response: QueryResponse,
|
||||
}
|
||||
|
||||
impl QueryResponseBuilder {
|
||||
pub fn new<T: JmapObject + Sync + Send>(
|
||||
total_results: usize,
|
||||
max_results: usize,
|
||||
query_state: State,
|
||||
request: &QueryRequest<T>,
|
||||
) -> Self {
|
||||
let (limit_total, limit) = if let Some(limit) = request.limit {
|
||||
if limit > 0 {
|
||||
let limit = std::cmp::min(limit, max_results);
|
||||
(std::cmp::min(limit, total_results), limit)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
} else {
|
||||
(std::cmp::min(max_results, total_results), max_results)
|
||||
};
|
||||
|
||||
QueryResponseBuilder {
|
||||
requested_position: request.position.unwrap_or(0),
|
||||
position: request.position.unwrap_or(0),
|
||||
limit: limit_total,
|
||||
has_anchor: request.anchor.is_some(),
|
||||
anchor: request.anchor.map(|anchor| anchor.id()).unwrap_or(0),
|
||||
anchor_offset: request.anchor_offset.unwrap_or(0),
|
||||
anchor_found: false,
|
||||
index: 0,
|
||||
response: QueryResponse {
|
||||
account_id: request.account_id,
|
||||
query_state,
|
||||
can_calculate_changes: true,
|
||||
position: 0,
|
||||
ids: vec![],
|
||||
total: if request.calculate_total.unwrap_or(false) {
|
||||
Some(total_results)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
limit: if total_results > limit {
|
||||
Some(limit)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add(&mut self, prefix_id: u32, document_id: u32) -> bool {
|
||||
self.add_id(Id::from_parts(prefix_id, document_id))
|
||||
}
|
||||
|
||||
pub fn add_id(&mut self, id: Id) -> bool {
|
||||
let id_u64 = id.id();
|
||||
|
||||
// Pagination
|
||||
if !self.has_anchor {
|
||||
if self.position >= 0 {
|
||||
if self.position > 0 {
|
||||
self.position -= 1;
|
||||
} else {
|
||||
self.response.ids.push(id);
|
||||
if self.response.ids.len() == self.limit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.response.ids.push(id);
|
||||
}
|
||||
} else {
|
||||
let current_index = self.index;
|
||||
self.index += 1;
|
||||
|
||||
if id_u64 == self.anchor {
|
||||
self.anchor_found = true;
|
||||
self.position = (current_index + self.anchor_offset).max(0);
|
||||
}
|
||||
|
||||
if self.anchor_offset >= 0 {
|
||||
if self.anchor_found && current_index >= self.position {
|
||||
self.response.ids.push(id);
|
||||
if self.limit > 0 && self.response.ids.len() == self.limit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.response.ids.push(id);
|
||||
if self.anchor_found
|
||||
&& self.limit > 0
|
||||
&& self.response.ids.len() >= self.position as usize + self.limit
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.response.ids.len() == self.limit
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> trc::Result<QueryResponse> {
|
||||
if self.has_anchor {
|
||||
if !self.anchor_found {
|
||||
return Err(trc::JmapEvent::AnchorNotFound.into_err());
|
||||
}
|
||||
|
||||
let start = self.position.max(0) as usize;
|
||||
if self.anchor_offset < 0 {
|
||||
let start = start.min(self.response.ids.len());
|
||||
let end = if self.limit > 0 {
|
||||
std::cmp::min(start + self.limit, self.response.ids.len())
|
||||
} else {
|
||||
self.response.ids.len()
|
||||
};
|
||||
self.response.ids = self.response.ids[start..end].to_vec();
|
||||
}
|
||||
self.response.position = start as i32;
|
||||
|
||||
return Ok(self.response);
|
||||
}
|
||||
|
||||
if self.requested_position >= 0 {
|
||||
self.response.position = if self.position == 0 {
|
||||
self.requested_position
|
||||
} else {
|
||||
0
|
||||
};
|
||||
} else {
|
||||
let position = self.position.unsigned_abs() as usize;
|
||||
let start_offset = if position < self.response.ids.len() {
|
||||
self.response.ids.len() - position
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.response.position = start_offset as i32;
|
||||
let end_offset = if self.limit > 0 {
|
||||
std::cmp::min(start_offset + self.limit, self.response.ids.len())
|
||||
} else {
|
||||
self.response.ids.len()
|
||||
};
|
||||
|
||||
self.response.ids = self.response.ids[start_offset..end_offset].to_vec();
|
||||
}
|
||||
|
||||
Ok(self.response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
addressbook::{get::AddressBookGet, set::AddressBookSet},
|
||||
api::auth::JmapAuthorization,
|
||||
blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload},
|
||||
calendar::{get::CalendarGet, set::CalendarSet},
|
||||
calendar_event::{
|
||||
copy::JmapCalendarEventCopy, get::CalendarEventGet, parse::CalendarEventParse,
|
||||
query::CalendarEventQuery, set::CalendarEventSet,
|
||||
},
|
||||
calendar_event_notification::{
|
||||
get::CalendarEventNotificationGet, query::CalendarEventNotificationQuery,
|
||||
set::CalendarEventNotificationSet,
|
||||
},
|
||||
changes::{get::ChangesLookup, query::QueryChanges},
|
||||
contact::{
|
||||
copy::JmapContactCardCopy, get::ContactCardGet, parse::ContactCardParse,
|
||||
query::ContactCardQuery, set::ContactCardSet,
|
||||
},
|
||||
email::{
|
||||
copy::JmapEmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse,
|
||||
query::EmailQuery, set::EmailSet, snippet::EmailSearchSnippet,
|
||||
},
|
||||
file::{copy::FileNodeCopy, get::FileNodeGet, query::FileNodeQuery, set::FileNodeSet},
|
||||
identity::{get::IdentityGet, set::IdentitySet},
|
||||
mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet},
|
||||
participant_identity::{get::ParticipantIdentityGet, set::ParticipantIdentitySet},
|
||||
principal::{availability::PrincipalGetAvailability, get::PrincipalGet, query::PrincipalQuery},
|
||||
push::{get::PushSubscriptionFetch, set::PushSubscriptionSet},
|
||||
quota::{get::QuotaGet, query::QuotaQuery},
|
||||
registry::{get::RegistryGet, query::RegistryQuery, set::RegistrySet},
|
||||
share_notification::{
|
||||
get::ShareNotificationGet, query::ShareNotificationQuery, set::ShareNotificationSet,
|
||||
},
|
||||
sieve::{
|
||||
get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet,
|
||||
validate::SieveScriptValidate,
|
||||
},
|
||||
submission::{get::EmailSubmissionGet, query::EmailSubmissionQuery, set::EmailSubmissionSet},
|
||||
thread::get::ThreadGet,
|
||||
vacation::{get::VacationResponseGet, set::VacationResponseSet},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
request::{
|
||||
Call, CopyRequestMethod, GetRequestMethod, INVALID_ACCOUNT_ID, ParseRequestMethod,
|
||||
QueryRequestMethod, Request, RequestMethod, SetRequestMethod,
|
||||
capability::Capability,
|
||||
method::{MethodName, MethodObject},
|
||||
},
|
||||
response::{Response, ResponseMethod, SetResponseMethod},
|
||||
};
|
||||
use std::future::Future;
|
||||
use std::time::Instant;
|
||||
use trc::JmapEvent;
|
||||
use types::{collection::Collection, id::Id};
|
||||
|
||||
pub trait RequestHandler: Sync + Send {
|
||||
fn handle_jmap_request<'x>(
|
||||
&self,
|
||||
request: Request<'x>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = Response<'x>> + Send;
|
||||
|
||||
fn handle_method_call<'x>(
|
||||
&self,
|
||||
method: RequestMethod<'x>,
|
||||
method_name: MethodName,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<ResponseMethod<'x>>> + Send;
|
||||
}
|
||||
|
||||
impl RequestHandler for Server {
|
||||
async fn handle_jmap_request<'x>(
|
||||
&self,
|
||||
request: Request<'x>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> Response<'x> {
|
||||
let add_created_ids = request.created_ids.is_some();
|
||||
let using = request.using;
|
||||
let mut response = Response::new(
|
||||
access_token.state(),
|
||||
request.created_ids.unwrap_or_default(),
|
||||
request.method_calls.len(),
|
||||
);
|
||||
|
||||
for mut call in request.method_calls {
|
||||
// Resolve result and id references
|
||||
if let Err(error) = response.resolve_references(&mut call.method) {
|
||||
let method_error = error.clone();
|
||||
|
||||
trc::error!(error.span_id(session.session_id));
|
||||
|
||||
response.push_response(call.id, MethodName::error(), method_error);
|
||||
continue;
|
||||
}
|
||||
|
||||
if !matches!(call.method, RequestMethod::Error(_)) {
|
||||
let capability = call.name.obj.capability();
|
||||
if capability != Capability::Stalwart && !using.contains(capability) {
|
||||
response.push_response(
|
||||
call.id,
|
||||
MethodName::error(),
|
||||
trc::JmapEvent::UnknownMethod.into_err().details(format!(
|
||||
"Method {} requires capability {} which is not present in the \"using\" property.",
|
||||
call.name,
|
||||
capability.as_str()
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut next_call = None;
|
||||
|
||||
// Add response
|
||||
let method_name = call.name.as_str();
|
||||
match self
|
||||
.handle_method_call(
|
||||
call.method,
|
||||
call.name,
|
||||
access_token,
|
||||
&mut next_call,
|
||||
session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(mut method_response) => {
|
||||
match &mut method_response {
|
||||
ResponseMethod::Set(set_response) => {
|
||||
// Add created ids
|
||||
match set_response {
|
||||
SetResponseMethod::Email(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::Mailbox(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::Identity(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::EmailSubmission(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::PushSubscription(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::Sieve(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::VacationResponse(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::AddressBook(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::ContactCard(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::FileNode(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::ShareNotification(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::Calendar(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::CalendarEvent(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::ParticipantIdentity(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
SetResponseMethod::CalendarEventNotification(_) => {}
|
||||
SetResponseMethod::Registry(set_response) => {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseMethod::ImportEmail(import_response) => {
|
||||
// Add created ids
|
||||
import_response.update_created_ids(&mut response);
|
||||
}
|
||||
ResponseMethod::UploadBlob(upload_response) => {
|
||||
// Add created blobIds
|
||||
upload_response.update_created_ids(&mut response);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
response.push_response(call.id, call.name, method_response);
|
||||
}
|
||||
Err(error) => {
|
||||
let method_error = error.clone();
|
||||
|
||||
trc::error!(
|
||||
error
|
||||
.span_id(session.session_id)
|
||||
.ctx_unique(trc::Key::AccountId, access_token.account_id())
|
||||
.caused_by(method_name)
|
||||
);
|
||||
|
||||
response.push_error(call.id, method_error);
|
||||
}
|
||||
}
|
||||
|
||||
// Process next call
|
||||
if let Some(next_call) = next_call {
|
||||
call = next_call;
|
||||
call.id
|
||||
.clone_from(&response.method_responses.last().unwrap().id);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !add_created_ids {
|
||||
response.created_ids.clear();
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
async fn handle_method_call<'x>(
|
||||
&self,
|
||||
method: RequestMethod<'x>,
|
||||
method_name: MethodName,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> trc::Result<ResponseMethod<'x>> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Check permissions
|
||||
access_token.assert_has_jmap_permission(&method, method_name.obj)?;
|
||||
|
||||
// Handle method
|
||||
let response = match method {
|
||||
RequestMethod::Get(req) => match req {
|
||||
GetRequestMethod::Email(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Mailbox(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
|
||||
|
||||
self.mailbox_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Thread(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.thread_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Identity(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.identity_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::EmailSubmission(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.email_submission_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::PushSubscription(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
self.push_subscription_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Sieve(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.sieve_script_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::VacationResponse(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.vacation_response_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::Principal(req) => {
|
||||
self.principal_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Quota(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.quota_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::Blob(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::AddressBook(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::AddressBook)?;
|
||||
|
||||
self.address_book_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::ContactCard(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::ContactCard)?;
|
||||
|
||||
self.contact_card_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::FileNode(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::PrincipalAvailability(req) => self
|
||||
.principal_get_availability(*req, access_token)
|
||||
.await?
|
||||
.into(),
|
||||
GetRequestMethod::Calendar(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Calendar)?;
|
||||
|
||||
self.calendar_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::CalendarEvent(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?;
|
||||
|
||||
self.calendar_event_get(*req, access_token).await?.into()
|
||||
}
|
||||
GetRequestMethod::CalendarEventNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.calendar_event_notification_get(*req, access_token)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
GetRequestMethod::ParticipantIdentity(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.participant_identity_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::ShareNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.share_notification_get(*req).await?.into()
|
||||
}
|
||||
GetRequestMethod::Registry(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
Box::pin(self.registry_get(
|
||||
method_name.obj.unwrap_registry(),
|
||||
*req,
|
||||
access_token,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Query(req) => match req {
|
||||
QueryRequestMethod::Email(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Mailbox(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
|
||||
|
||||
self.mailbox_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::EmailSubmission(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.email_submission_query(*req).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Sieve(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.sieve_script_query(*req).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Principal(req) => {
|
||||
self.principal_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Quota(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.quota_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::AddressBook(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::AddressBook)?;
|
||||
|
||||
self.address_book_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::ContactCard(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::ContactCard)?;
|
||||
|
||||
self.contact_card_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::FileNode(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Calendar(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Calendar)?;
|
||||
|
||||
self.calendar_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::CalendarEvent(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?;
|
||||
|
||||
self.calendar_event_query(*req, access_token).await?.into()
|
||||
}
|
||||
QueryRequestMethod::CalendarEventNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.calendar_event_notification_query(*req, access_token)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
QueryRequestMethod::ShareNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.share_notification_query(*req).await?.into()
|
||||
}
|
||||
QueryRequestMethod::Registry(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
Box::pin(self.registry_query(
|
||||
method_name.obj.unwrap_registry(),
|
||||
*req,
|
||||
access_token,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Set(req) => match req {
|
||||
SetRequestMethod::Email(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_set(*req, access_token, session).await?.into()
|
||||
}
|
||||
SetRequestMethod::Mailbox(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
|
||||
|
||||
self.mailbox_set(*req, access_token).await?.into()
|
||||
}
|
||||
SetRequestMethod::Identity(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.identity_set(*req).await?.into()
|
||||
}
|
||||
SetRequestMethod::EmailSubmission(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.email_submission_set(*req, &session.instance, next_call)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::PushSubscription(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
self.push_subscription_set(*req, access_token).await?.into()
|
||||
}
|
||||
SetRequestMethod::Sieve(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.sieve_script_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::VacationResponse(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.vacation_response_set(*req, access_token).await?.into()
|
||||
}
|
||||
SetRequestMethod::AddressBook(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::AddressBook)?;
|
||||
|
||||
self.address_book_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::ContactCard(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::ContactCard)?;
|
||||
|
||||
self.contact_card_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::FileNode(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::ShareNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.share_notification_set(*req).await?.into()
|
||||
}
|
||||
SetRequestMethod::Calendar(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Calendar)?;
|
||||
|
||||
self.calendar_set(*req, access_token, session).await?.into()
|
||||
}
|
||||
SetRequestMethod::CalendarEvent(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?;
|
||||
|
||||
self.calendar_event_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::CalendarEventNotification(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.calendar_event_notification_set(*req, access_token, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
SetRequestMethod::ParticipantIdentity(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.participant_identity_set(*req).await?.into()
|
||||
}
|
||||
SetRequestMethod::Registry(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
Box::pin(self.registry_set(
|
||||
method_name.obj.unwrap_registry(),
|
||||
*req,
|
||||
access_token,
|
||||
session,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Changes(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
|
||||
self.changes(*req, method_name.obj, access_token)
|
||||
.await?
|
||||
.into_method_response()
|
||||
}
|
||||
RequestMethod::Copy(req) => match req {
|
||||
CopyRequestMethod::Email(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
resolve_account_id(&mut req.from_account_id, method_name.obj, access_token)?;
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::Email)?
|
||||
.assert_has_access(req.from_account_id, Collection::Email)?;
|
||||
|
||||
self.email_copy(*req, access_token, next_call, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
CopyRequestMethod::Blob(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_copy(*req, access_token).await?.into()
|
||||
}
|
||||
CopyRequestMethod::ContactCard(mut req) => {
|
||||
resolve_account_id(&mut req.from_account_id, method_name.obj, access_token)?;
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::ContactCard)?
|
||||
.assert_has_access(req.from_account_id, Collection::ContactCard)?;
|
||||
|
||||
self.contact_card_copy(*req, access_token, next_call, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
CopyRequestMethod::CalendarEvent(mut req) => {
|
||||
resolve_account_id(&mut req.from_account_id, method_name.obj, access_token)?;
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::CalendarEvent)?
|
||||
.assert_has_access(req.from_account_id, Collection::CalendarEvent)?;
|
||||
|
||||
self.calendar_event_copy(*req, access_token, next_call, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
CopyRequestMethod::FileNode(mut req) => {
|
||||
resolve_account_id(&mut req.from_account_id, method_name.obj, access_token)?;
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::FileNode)?
|
||||
.assert_has_access(req.from_account_id, Collection::FileNode)?;
|
||||
|
||||
self.file_node_copy(*req, access_token, next_call, session)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::ImportEmail(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_import(*req, access_token, session).await?.into()
|
||||
}
|
||||
RequestMethod::Parse(req) => match req {
|
||||
ParseRequestMethod::Email(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_parse(*req, access_token).await?.into()
|
||||
}
|
||||
ParseRequestMethod::ContactCard(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::ContactCard)?;
|
||||
|
||||
self.contact_card_parse(*req, access_token).await?.into()
|
||||
}
|
||||
ParseRequestMethod::CalendarEvent(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?;
|
||||
|
||||
self.calendar_event_parse(*req, access_token).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::QueryChanges(req) => self.query_changes(req, access_token).await?.into(),
|
||||
RequestMethod::SearchSnippet(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
self.email_search_snippet(*req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::ValidateScript(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.sieve_script_validate(*req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::LookupBlob(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_lookup(*req).await?.into()
|
||||
}
|
||||
RequestMethod::UploadBlob(mut req) => {
|
||||
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_upload_many(*req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::Echo(req) => req.into(),
|
||||
RequestMethod::Error(error) => return Err(error),
|
||||
};
|
||||
|
||||
trc::event!(
|
||||
Jmap(JmapEvent::MethodCall),
|
||||
Id = method_name.as_str(),
|
||||
SpanId = session.session_id,
|
||||
AccountId = access_token.account_id(),
|
||||
Elapsed = op_start.elapsed(),
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_account_id(
|
||||
account_id: &mut Id,
|
||||
obj: MethodObject,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<()> {
|
||||
if account_id.id() < INVALID_ACCOUNT_ID {
|
||||
Ok(())
|
||||
} else if matches!(
|
||||
obj,
|
||||
MethodObject::Core | MethodObject::PushSubscription | MethodObject::Registry(_)
|
||||
) {
|
||||
*account_id = Id::from(access_token.account_id());
|
||||
Ok(())
|
||||
} else if account_id.id() == INVALID_ACCOUNT_ID {
|
||||
Err(trc::JmapEvent::AccountNotFound.into_err())
|
||||
} else {
|
||||
Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("The \"accountId\" property is required."))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::request::capability::{
|
||||
Account, Capabilities, Capability, EmptyCapabilities, Session,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::future::Future;
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait SessionHandler: Sync + Send {
|
||||
fn handle_session_resource(
|
||||
&self,
|
||||
base_url: String,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<Session>> + Send;
|
||||
}
|
||||
|
||||
impl SessionHandler for Server {
|
||||
async fn handle_session_resource(
|
||||
&self,
|
||||
base_url: String,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<Session> {
|
||||
let mut session = Session::new(base_url, &self.core.jmap.capabilities);
|
||||
session.set_state(access_token.state());
|
||||
let account_capabilities = &self.core.jmap.capabilities.account;
|
||||
|
||||
// Set primary account
|
||||
let account = self
|
||||
.account(access_token.account_id())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
session.username = account.name().to_string();
|
||||
let account_id = Id::from(access_token.account_id());
|
||||
let mut account = Account {
|
||||
name: account.name().to_string(),
|
||||
is_personal: true,
|
||||
is_read_only: false,
|
||||
account_capabilities: VecMap::with_capacity(account_capabilities.len()),
|
||||
};
|
||||
for capability in access_token.account_capabilities() {
|
||||
session.primary_accounts.append(capability, account_id);
|
||||
account.account_capabilities.append(
|
||||
capability,
|
||||
account_capabilities
|
||||
.get(&capability)
|
||||
.map(|v| v.to_account_capabilities(account_id.into(), true))
|
||||
.unwrap_or_else(|| Capabilities::Empty(EmptyCapabilities::default())),
|
||||
);
|
||||
}
|
||||
session.accounts.append(account_id, account);
|
||||
|
||||
// Add secondary accounts
|
||||
for &account_id in access_token.secondary_ids() {
|
||||
let is_owner = access_token.is_member(account_id);
|
||||
let Some(account) = self
|
||||
.try_account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
trc::event!(
|
||||
Auth(trc::AuthEvent::Warning),
|
||||
AccountId = account_id,
|
||||
Reason = "Skipping orphan secondary account id in session",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let account_id = Id::from(account_id);
|
||||
let mut account = Account {
|
||||
name: account.name().to_string(),
|
||||
is_personal: false,
|
||||
is_read_only: false,
|
||||
account_capabilities: VecMap::with_capacity(account_capabilities.len()),
|
||||
};
|
||||
for capability in access_token.account_capabilities() {
|
||||
account.account_capabilities.append(
|
||||
capability,
|
||||
account_capabilities
|
||||
.get(&capability)
|
||||
.map(|v| v.to_account_capabilities(account_id.into(), is_owner))
|
||||
.unwrap_or_else(|| Capabilities::Empty(EmptyCapabilities::default())),
|
||||
);
|
||||
}
|
||||
session.accounts.append(account_id, account);
|
||||
}
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
trait AccountCapabilities {
|
||||
fn account_capabilities(&self) -> impl Iterator<Item = Capability>;
|
||||
}
|
||||
|
||||
impl AccountCapabilities for AccessToken {
|
||||
fn account_capabilities(&self) -> impl Iterator<Item = Capability> {
|
||||
Capability::all_capabilities()
|
||||
.iter()
|
||||
.filter(move |capability| {
|
||||
let permission = match capability {
|
||||
Capability::Mail | Capability::MailShare | Capability::EmailPush => {
|
||||
Permission::JmapEmailGet
|
||||
}
|
||||
Capability::Submission => Permission::JmapEmailSubmissionCreate,
|
||||
Capability::VacationResponse => Permission::JmapVacationResponseGet,
|
||||
Capability::Contacts => Permission::JmapContactCardGet,
|
||||
Capability::ContactsParse => Permission::JmapContactCardParse,
|
||||
Capability::Calendars => Permission::JmapCalendarEventGet,
|
||||
Capability::CalendarsParse => Permission::JmapCalendarEventParse,
|
||||
Capability::Sieve => Permission::JmapSieveScriptGet,
|
||||
Capability::Blob => Permission::JmapBlobGet,
|
||||
Capability::Quota => Permission::JmapQuotaGet,
|
||||
Capability::FileNode => Permission::JmapFileNodeGet,
|
||||
Capability::WebSocket
|
||||
| Capability::Principals
|
||||
| Capability::PrincipalsAvailability
|
||||
| Capability::Stalwart => return true,
|
||||
Capability::Core | Capability::PrincipalsOwner | Capability::WebPushVapid => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
self.has_permission(permission)
|
||||
})
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user