Files
inbuxa-server/crates/jmap/src/api/request.rs
T
jcoffey-dev 6a53d47106 Mark the files this fork changed (AGPL section 5(a))
The AGPL asks a modified version to carry prominent notices saying it was
modified, and giving a date. Publishing the source is the conveyance that
asks for it, so it wants doing before the repository is public rather than
at the release.

Every upstream file the fork changed now says so in its header, beneath the
notice it came with: 164 files, found by diffing against the upstream
snapshot branch rather than by guessing, so the list is what actually
differs. Files the fork wrote itself already carry their own copyright and
need nothing. Upstream's notices are untouched, which its licence requires
and which was already true.

The README says the same thing in prose, since the obligation is on the
work as a whole and not only its Rust files.

Builds unchanged: the server and the test binary both compile.
2026-09-19 23:48:35 -07:00

880 lines
41 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::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()
}
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()
}
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."))
}
}