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.
880 lines
41 KiB
Rust
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."))
|
|
}
|
|
}
|