Files
Stalwart/crates/jmap/src/api/request.rs
2024-09-26 14:49:46 +02:00

379 lines
14 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::Instant};
use common::{auth::AccessToken, Server};
use jmap_proto::{
method::{
get, query,
set::{self},
},
request::{method::MethodName, Call, Request, RequestMethod},
response::{Response, ResponseMethod},
types::collection::Collection,
};
use trc::JmapEvent;
use crate::{
blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload},
changes::{get::ChangesLookup, query::QueryChanges},
email::{
copy::EmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse, query::EmailQuery,
set::EmailSet, snippet::EmailSearchSnippet,
},
identity::{get::IdentityGet, set::IdentitySet},
mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet},
principal::{get::PrincipalGet, query::PrincipalQuery},
push::{get::PushSubscriptionFetch, set::PushSubscriptionSet},
quota::{get::QuotaGet, query::QuotaQuery},
services::state::StateManager,
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 super::http::HttpSessionData;
use std::future::Future;
pub trait RequestHandler: Sync + Send {
fn handle_request(
&self,
request: Request,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
) -> impl Future<Output = Response> + Send;
fn handle_method_call(
&self,
method: RequestMethod,
method_name: &'static str,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<ResponseMethod>> + Send;
}
impl RequestHandler for Server {
async fn handle_request(
&self,
request: Request,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
) -> Response {
let mut response = Response::new(
access_token.state(),
request.created_ids.unwrap_or_default(),
request.method_calls.len(),
);
let add_created_ids = !response.created_ids.is_empty();
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;
}
loop {
let mut next_call = None;
// Add response
let method_name = call.name.as_str();
match self
.handle_method_call(
call.method,
method_name,
&access_token,
&mut next_call,
session,
)
.await
{
Ok(mut method_response) => {
match &mut method_response {
ResponseMethod::Set(set_response) => {
// Add created ids
set_response.update_created_ids(&mut response);
// Publish state changes
if let Some(state_change) = set_response.state_change.take() {
self.broadcast_state_change(state_change).await;
}
}
ResponseMethod::ImportEmail(import_response) => {
// Add created ids
import_response.update_created_ids(&mut response);
// Publish state changes
if let Some(state_change) = import_response.state_change.take() {
self.broadcast_state_change(state_change).await;
}
}
ResponseMethod::Copy(copy_response) => {
// Publish state changes
if let Some(state_change) = copy_response.state_change.take() {
self.broadcast_state_change(state_change).await;
}
}
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.primary_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(
&self,
method: RequestMethod,
method_name: &'static str,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>,
session: &HttpSessionData,
) -> trc::Result<ResponseMethod> {
let op_start = Instant::now();
// Check permissions
access_token.assert_has_jmap_permission(&method)?;
// Handle method
let response = match method {
RequestMethod::Get(mut req) => match req.take_arguments() {
get::RequestArguments::Email(arguments) => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_get(req.with_arguments(arguments), access_token)
.await?
.into()
}
get::RequestArguments::Mailbox => {
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
self.mailbox_get(req, access_token).await?.into()
}
get::RequestArguments::Thread => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.thread_get(req).await?.into()
}
get::RequestArguments::Identity => {
access_token.assert_is_member(req.account_id)?;
self.identity_get(req).await?.into()
}
get::RequestArguments::EmailSubmission => {
access_token.assert_is_member(req.account_id)?;
self.email_submission_get(req).await?.into()
}
get::RequestArguments::PushSubscription => {
self.push_subscription_get(req, access_token).await?.into()
}
get::RequestArguments::SieveScript => {
access_token.assert_is_member(req.account_id)?;
self.sieve_script_get(req).await?.into()
}
get::RequestArguments::VacationResponse => {
access_token.assert_is_member(req.account_id)?;
self.vacation_response_get(req).await?.into()
}
get::RequestArguments::Principal => self.principal_get(req).await?.into(),
get::RequestArguments::Quota => {
access_token.assert_is_member(req.account_id)?;
self.quota_get(req, access_token).await?.into()
}
get::RequestArguments::Blob(arguments) => {
access_token.assert_is_member(req.account_id)?;
self.blob_get(req.with_arguments(arguments), access_token)
.await?
.into()
}
},
RequestMethod::Query(mut req) => match req.take_arguments() {
query::RequestArguments::Email(arguments) => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_query(req.with_arguments(arguments), access_token)
.await?
.into()
}
query::RequestArguments::Mailbox(arguments) => {
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
self.mailbox_query(req.with_arguments(arguments), access_token)
.await?
.into()
}
query::RequestArguments::EmailSubmission => {
access_token.assert_is_member(req.account_id)?;
self.email_submission_query(req).await?.into()
}
query::RequestArguments::SieveScript => {
access_token.assert_is_member(req.account_id)?;
self.sieve_script_query(req).await?.into()
}
query::RequestArguments::Principal => {
self.principal_query(req, session).await?.into()
}
query::RequestArguments::Quota => {
access_token.assert_is_member(req.account_id)?;
self.quota_query(req, access_token).await?.into()
}
},
RequestMethod::Set(mut req) => match req.take_arguments() {
set::RequestArguments::Email => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_set(req, access_token, session).await?.into()
}
set::RequestArguments::Mailbox(arguments) => {
access_token.assert_has_access(req.account_id, Collection::Mailbox)?;
self.mailbox_set(req.with_arguments(arguments), access_token)
.await?
.into()
}
set::RequestArguments::Identity => {
access_token.assert_is_member(req.account_id)?;
self.identity_set(req).await?.into()
}
set::RequestArguments::EmailSubmission(arguments) => {
access_token.assert_is_member(req.account_id)?;
self.email_submission_set(
req.with_arguments(arguments),
&session.instance,
next_call,
)
.await?
.into()
}
set::RequestArguments::PushSubscription => {
self.push_subscription_set(req, access_token).await?.into()
}
set::RequestArguments::SieveScript(arguments) => {
access_token.assert_is_member(req.account_id)?;
self.sieve_script_set(req.with_arguments(arguments), access_token, session)
.await?
.into()
}
set::RequestArguments::VacationResponse => {
access_token.assert_is_member(req.account_id)?;
self.vacation_response_set(req, access_token).await?.into()
}
},
RequestMethod::Changes(req) => self.changes(req, access_token).await?.into(),
RequestMethod::Copy(req) => {
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()
}
RequestMethod::ImportEmail(req) => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_import(req, access_token, session).await?.into()
}
RequestMethod::ParseEmail(req) => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_parse(req, access_token).await?.into()
}
RequestMethod::QueryChanges(req) => self.query_changes(req, access_token).await?.into(),
RequestMethod::SearchSnippet(req) => {
access_token.assert_has_access(req.account_id, Collection::Email)?;
self.email_search_snippet(req, access_token).await?.into()
}
RequestMethod::ValidateScript(req) => {
access_token.assert_is_member(req.account_id)?;
self.sieve_script_validate(req, access_token).await?.into()
}
RequestMethod::CopyBlob(req) => {
access_token.assert_is_member(req.account_id)?;
self.blob_copy(req, access_token).await?.into()
}
RequestMethod::LookupBlob(req) => {
access_token.assert_is_member(req.account_id)?;
self.blob_lookup(req).await?.into()
}
RequestMethod::UploadBlob(req) => {
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,
SpanId = session.session_id,
AccountId = access_token.primary_id(),
Elapsed = op_start.elapsed(),
);
Ok(response)
}
}