Files
inbuxa-server/crates/jmap/src/api/request.rs
T
jcoffey-dev b65afb66f9 A tenant can turn legacy protocols off for itself (LP-9 to LP-14a)
The tenant switch. A tenant's administrator turns legacy mail protocols
off for its own tenant, and from then on sign-in over IMAP, POP3,
ManageSieve and SMTP AUTH is refused for every address on the tenant's
domains, while every other domain on the server carries on. No port
closes, since other tenants share them (LP-13): it is one stored fact per
tenant, read at sign-in and when client configuration is answered.

inbuxa:TenantProtocolPolicy/get and /set, one per tenant, id the tenant's:

- Inside a tenant, a principal reaches only its own tenant's switch
  (MT-1): /get with no ids answers with it, another tenant's is notFound
  and can't be changed. At server level /get with no ids lists every
  tenant's.
- Turning it off is always allowed. Turning it back on is refused with
  forbidden, naming inbuxa:ProtocolPolicy, while the server has legacy
  protocols off (LP-9).
- A change raises security.legacy-protocols-changed with policy = tenant,
  the tenant's id, the new value and who made it (LP-14).
- It takes sysDomainGet and sysDomainUpdate, not the two new permissions
  the spec names. The switch governs sign-in on the tenant's domains, so
  whoever manages those domains may turn it -- and the default Tenant
  Administrator role already holds both, where new permissions would reach
  no role already stored on a server (MT-12's note), leaving today's
  tenant administrators without the switch until someone edited their
  role by hand. The same trade inbuxa:AiLimits and inbuxa:ProtocolPolicy
  made. /query is not built yet; /get with no ids covers listing.

Sign-in (LP-10 to LP-12). Before the credentials are looked at, the name
given is resolved to its domain and the domain to its tenant, so a real
account and a made-up address on the domain get the same refusal, with a
right password or a wrong one, counted as no failed sign-in (LP-11). The
words are the spec's: "Your organization allows only INBUXA webmail and
JMAP apps...", in each protocol's form. A bearer token needn't name an
account, so after authentication the account's own tenant is checked too;
a token that named nobody can't slip past.

The refusal carries policy = tenant and the domain, not the tenant's id:
IMAP answers a command's tag from the Id key, so an error holding one was
sent under the wrong tag and the mail app hung waiting for its reply. The
first live run found that; a unit test now holds the refusal to it.

Client configuration (LP-14a). Autoconfig, autodiscover, PACC and the
suggested DNS records now ask whether legacy services are off for the
domain being answered for -- the server's switch, or the domain's
tenant's -- so a tenant's domains stop offering IMAP, POP3 and
submission while others still do.

tests/e2e/legacy_protocols.py builds a tenant with its own domain, a user
and a tenant administrator, and a second tenant, and proves on a running
server: the admin sees and changes only its own tenant's switch (test 10);
turning it off is an event (test 14); the tenant's user is refused over
IMAP with the right password and a wrong one, a made-up address on the
domain the same (tests 6, 7); POP3 and submission refuse in their own
forms and JMAP still works (test 8); an account on another domain signs in
normally (test 6); autoconfig drops IMAP for the tenant's domain only; with
the server off, the tenant can't turn it back on (test 9); and once back
on, the user signs in again. All 62 checks pass.
2026-09-21 11:18:42 -07:00

914 lines
43 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
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(),
);
// inbuxa: ST-6: reads before the request's first write may go to a
// read replica
let mut has_written = false;
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();
// inbuxa: ST-6, ST-7: a read before the first write may use a
// replica that has every change the client has seen
let eligible = !has_written
&& matches!(
call.method,
RequestMethod::Get(_)
| RequestMethod::Query(_)
| RequestMethod::Changes(_)
| RequestMethod::QueryChanges(_)
);
if matches!(
call.method,
RequestMethod::Set(_)
| RequestMethod::Copy(_)
| RequestMethod::ImportEmail(_)
| RequestMethod::UploadBlob(_)
) {
has_written = true;
}
let presented = match &call.method {
RequestMethod::Changes(changes) => match &changes.since_state {
jmap_proto::types::state::State::Exact(change_id) => {
Some((changes.account_id.document_id(), *change_id))
}
_ => None,
},
_ => None,
};
let method_call = self.handle_method_call(
call.method,
call.name,
access_token,
&mut next_call,
session,
);
let result = if eligible {
store::backend::scaleout::replica::replica_read(
access_token.all_ids().map(|account_id| {
(
account_id,
presented
.filter(|(id, _)| *id == account_id)
.map_or(0, |(_, change_id)| change_id),
)
}),
method_call,
)
.await
} else {
method_call.await
};
match result
{
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::MaskedEmail(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::DeletedAccount(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::AiLimits(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::ProtocolPolicy(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::TenantProtocolPolicy(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()
}
// inbuxa: Fastmail's MaskedEmail/get
GetRequestMethod::MaskedEmail(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::fastmail::get(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:DeletedAccount/get (UD-17)
GetRequestMethod::DeletedAccount(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::deleted_account::get(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:AiLimits/get
GetRequestMethod::AiLimits(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::ai_limits::get(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:ProtocolPolicy/get (legacy protocols off)
GetRequestMethod::ProtocolPolicy(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::protocol_policy::get(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:TenantProtocolPolicy/get (legacy protocols off, per tenant)
GetRequestMethod::TenantProtocolPolicy(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::tenant_protocol_policy::get(self, access_token, *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)?;
assert_registry_account(self, method_name.obj, access_token, req.account_id)
.await?;
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)?;
assert_registry_account(self, method_name.obj, access_token, req.account_id)
.await?;
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()
}
// inbuxa: Fastmail's MaskedEmail/set
SetRequestMethod::MaskedEmail(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::fastmail::set(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:DeletedAccount/set (UD-17)
SetRequestMethod::DeletedAccount(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::deleted_account::set(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:AiLimits/set
SetRequestMethod::AiLimits(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::ai_limits::set(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:ProtocolPolicy/set (legacy protocols off)
SetRequestMethod::ProtocolPolicy(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::protocol_policy::set(self, access_token, *req)
.await?
.into()
}
// inbuxa: inbuxa:TenantProtocolPolicy/set (legacy protocols off, per tenant)
SetRequestMethod::TenantProtocolPolicy(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::tenant_protocol_policy::set(self, access_token, *req)
.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)?;
assert_registry_account(self, method_name.obj, access_token, req.account_id)
.await?;
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)?;
// inbuxa: x:MaskedEmail/changes and x:ArchivedItem/changes
if method_name.obj
== MethodObject::Registry(registry::schema::prelude::ObjectType::ArchivedItem)
{
crate::inbuxa::undelete::changes(self, access_token, *req).await?
} else if matches!(method_name.obj, MethodObject::Registry(_)) {
crate::inbuxa::masked_email::changes(self, access_token, *req).await?
} else {
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)
}
}
// inbuxa: ME-19: a tenant administrator reaches the masks of its tenant's
// accounts without impersonate, and a tenant principal never reaches beyond
// its tenant
async fn assert_registry_account(
server: &Server,
obj: MethodObject,
access_token: &AccessToken,
account_id: Id,
) -> trc::Result<()> {
if matches!(
obj,
MethodObject::Registry(
registry::schema::prelude::ObjectType::MaskedEmail
| registry::schema::prelude::ObjectType::ArchivedItem
)
) {
crate::inbuxa::masked_email::assert_can_manage(
server,
access_token,
account_id.document_id(),
)
.await
} else {
access_token.assert_is_member(account_id).map(|_| ())
}
}
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."))
}
}