Files
inbuxa-server/crates/jmap/src/api/request.rs
T
jcoffey-dev 68dd749291
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Successful in 4m47s
Export what a legal hold keeps as a ZIP (LH-12)
inbuxa:HoldExport/set takes a hold, optionally some of the accounts it
covers, and a reason; the collection runs in the background and get
says when it's ready. The ZIP has, per account, mail as .eml under its
folders, calendars as .ics, contacts as .vcf, files as stored, and the
archived items the hold keeps under archived/; a manifest.csv gives each
entry's account, kind, folder, date, whether it was archived, size and
SHA-256, and manifest.sha256 hashes the manifest. Accounts the hold
doesn't cover are left out, and items outside its date range are too:
live mail by arrival, events by start, and archived items the same way,
so an export doesn't carry deleted items that only another hold keeps.

The finished file is a blob of whoever started the export, so only they
download it, and it lasts as long as any upload (uploadTtl). Exports
are records under the hold (SUBSPACE_INBUXA H/e): never changed or
destroyed, each with its status, counts, size and checksum. Starting
one needs sysLegalHoldExport, an active hold and a reason, and is
recorded in the audit log like the audit log's own export.

The build is in memory and capped at 2 GB; bigger holds fail with a
message saying so, and are split by picking accounts.

Tested: unit tests for safe ZIP names and the manifest and its hash;
the legal_hold system test, on RocksDB, PostgreSQL and MySQL, exports a
hold end to end (live and archived mail, the manifest's hash, an asked-
for account the hold doesn't cover left out) and checks the refusals
(no reason, a user without the permission, a released hold) and the
audit record; and by hand from the console on a local server. Not
covered by a test: the archived-item date range with two holds of
different ranges over one account.
2026-09-27 21:45:52 -07:00

1193 lines
56 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(_)
);
let is_write = matches!(
call.method,
RequestMethod::Set(_)
| RequestMethod::Copy(_)
| RequestMethod::ImportEmail(_)
| RequestMethod::UploadBlob(_)
);
if is_write {
has_written = true;
}
// inbuxa: AL-7: what a delegate makes in a locked account
// may need the lock's grants
let makes_containers = is_write
&& matches!(
call.name.obj,
MethodObject::Mailbox
| MethodObject::Calendar
| MethodObject::AddressBook
| MethodObject::FileNode
);
let call_name = call.name.as_str().into_owned();
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,
};
// inbuxa: AU-1.6: which accounts it reached by impersonation
let method_call = crate::inbuxa::audit::collect_access(Box::pin(
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
};
let (result, reached) = result;
for account_id in reached {
// inbuxa: AL-9: a delegate's access, and what it
// changes, are recorded; anyone else here impersonated
if let Some(delegation) = access_token.delegation(account_id) {
let access = delegation.access.as_str();
self.audit_delegate(
access_token,
account_id,
access,
is_write.then_some(call_name.as_str()),
result.as_ref().err(),
)
.await;
if makes_containers
&& result.is_ok()
&& let Err(err) =
email::inbuxa_lock::reconcile(self, account_id).await
{
trc::error!(err.details("Failed to grant a lock's delegates on new folders"));
}
} else {
self.audit_foreign_access(access_token, account_id, false).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::AuditSettings(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::AuditExport(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::AuditVerification(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::AccountLock(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::LegalHold(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::HoldExport(set_response) => {
set_response.update_created_ids(&mut response);
}
SetResponseMethod::Explanation(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)?;
// inbuxa: AL-8: a delegate may send as a locked account
access_token.assert_can_send(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)?;
// inbuxa: AL-8: a delegate may send as a locked account
access_token.assert_can_send(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: account lock with delegation (AL-1)
GetRequestMethod::AccountLock(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::account_lock::get(self, access_token, *req)
.await?
.into()
}
// inbuxa: legal hold (LH-1)
// inbuxa: legal hold exports (LH-12)
GetRequestMethod::HoldExport(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::hold_export_api::get(self, *req).await?.into()
}
GetRequestMethod::LegalHold(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::legal_hold::get(self, *req).await?.into()
}
// inbuxa: the audit log (AU-9)
GetRequestMethod::AuditEvent(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit_log::event_get(self, access_token, *req)
.await?
.into()
}
GetRequestMethod::AuditSettings(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit_log::settings_get(self, *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)?;
// inbuxa: AL-8: a delegate may send as a locked account
access_token.assert_can_send(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()
}
// inbuxa: the audit log (AU-9)
QueryRequestMethod::AuditEvent(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit_log::event_query(self, access_token, *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)?;
// inbuxa: AL-8: a delegate may send as a locked account
access_token.assert_can_send(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)?;
// inbuxa: AU-1.2, AU-3
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(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)?;
// inbuxa: AU-1.2, AU-3
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(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)?;
// inbuxa: AU-1.2, AU-3
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(crate::inbuxa::ai_limits::set(self, access_token, req)),
)
.await?
.into()
}
// inbuxa: the audit log (AU-7, AU-11, AU-6)
SetRequestMethod::AuditSettings(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(crate::inbuxa::audit_log::settings_set(self, access_token, req)),
)
.await?
.into()
}
// inbuxa: account lock with delegation, recorded with its
// reason (AL-1, AU-12)
SetRequestMethod::AccountLock(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
let reason = req.arguments.reason.clone().or_else(|| {
req.create.as_ref().and_then(|create| {
create.values().find_map(|value| {
serde_json::to_value(value)
.ok()?
.get("reason")?
.as_str()
.map(str::to_string)
})
})
});
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
reason,
*req,
|req| Box::pin(crate::inbuxa::account_lock::set(self, access_token, req)),
)
.await?
.into()
}
// inbuxa: legal hold (LH-1), each change recorded with its
// reason (AU-12)
// inbuxa: legal hold exports, recorded with their reason
// (AU-1.9, AU-12)
SetRequestMethod::HoldExport(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
let reason = req.arguments.reason.clone().or_else(|| {
req.create.as_ref().and_then(|create| {
create.values().find_map(|value| {
serde_json::to_value(value)
.ok()?
.get("reason")?
.as_str()
.map(str::to_string)
})
})
});
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
reason,
*req,
|req| Box::pin(crate::inbuxa::hold_export_api::set(self, access_token, req)),
)
.await?
.into()
}
SetRequestMethod::LegalHold(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
let reason = req.arguments.reason.clone().or_else(|| {
req.create.as_ref().and_then(|create| {
create.values().find_map(|value| {
serde_json::to_value(value)
.ok()?
.get("reason")?
.as_str()
.map(str::to_string)
})
})
});
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
reason,
*req,
|req| Box::pin(crate::inbuxa::legal_hold::set(self, access_token, req)),
)
.await?
.into()
}
SetRequestMethod::AuditExport(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit_log::export_set(self, access_token, session, *req)
.await?
.into()
}
SetRequestMethod::AuditVerification(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::audit_log::verification_set(self, access_token, session, *req)
.await?
.into()
}
// inbuxa: inbuxa:Explanation/set ("Explain this")
SetRequestMethod::Explanation(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
crate::inbuxa::explanation::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)?;
// inbuxa: AU-1.2, AU-3
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(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)?;
// inbuxa: AU-1.2, AU-3
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
None,
None,
*req,
|req| Box::pin(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?;
// inbuxa: AU-1.1, AU-3: recorded before and after
let object_type = method_name.obj.unwrap_registry();
crate::inbuxa::audit::recorded(
self,
access_token,
session,
&method_name.obj.to_string(),
Some(object_type),
None,
*req,
|req| Box::pin(self.registry_set(object_type, 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 {
// inbuxa: AU-1.6
crate::inbuxa::audit::note_access(account_id.document_id(), access_token);
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."))
}
}