Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+278
View File
@@ -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.")),
}
}
}
+356
View File
@@ -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(())
}
}
+180
View File
@@ -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
};
}
}))))
}
}
+258
View File
@@ -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,
}
}
}
+176
View File
@@ -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)
}
}
+739
View File
@@ -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."))
}
}
+134
View File
@@ -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()
}
}