Files
Stalwart/crates/jmap-proto/src/request/parser.rs
2024-07-23 12:44:14 +02:00

353 lines
13 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::collections::HashMap;
use crate::{
method::{
changes::ChangesRequest,
copy::{CopyBlobRequest, CopyRequest},
get::GetRequest,
import::ImportEmailRequest,
lookup::BlobLookupRequest,
parse::ParseEmailRequest,
query::QueryRequest,
query_changes::QueryChangesRequest,
search_snippet::GetSearchSnippetRequest,
set::SetRequest,
upload::BlobUploadRequest,
validate::ValidateSieveScriptRequest,
},
parser::{json::Parser, Ignore, JsonObjectParser, Token},
types::any_id::AnyId,
};
use super::{
capability::Capability,
echo::Echo,
method::{MethodFunction, MethodName, MethodObject},
Call, Request, RequestMethod,
};
impl Request {
pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
if json.len() <= max_size {
let mut request = Request {
using: 0,
method_calls: Vec::new(),
created_ids: None,
};
let mut found_valid_keys = false;
let mut parser = Parser::new(json);
parser.next_token::<String>()?.assert(Token::DictStart)?;
while let Some(key) = parser.next_dict_key::<u128>()? {
found_valid_keys |= request.parse_key(&mut parser, max_calls, key)?;
}
if found_valid_keys {
Ok(request)
} else {
Err(trc::JmapEvent::NotRequest
.into_err()
.details("Invalid JMAP request"))
}
} else {
Err(trc::LimitEvent::SizeRequest.into_err())
}
}
pub(crate) fn parse_key(
&mut self,
parser: &mut Parser,
max_calls: usize,
key: u128,
) -> trc::Result<bool> {
match key {
0x0067_6e69_7375 => {
parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?;
loop {
match parser.next_token::<Capability>()? {
Token::String(capability) => {
self.using |= capability as u32;
}
Token::Comma => (),
Token::ArrayEnd => break,
token => return Err(token.error("capability", &token.to_string())),
}
}
Ok(true)
}
0x0073_6c6c_6143_646f_6874_656d => {
parser
.next_token::<Ignore>()?
.assert_jmap(Token::ArrayStart)?;
loop {
match parser.next_token::<Ignore>()? {
Token::ArrayStart => (),
Token::Comma => continue,
Token::ArrayEnd => break,
_ => {
return Err(trc::JmapEvent::NotRequest
.into_err()
.details("Invalid JMAP request"));
}
};
if self.method_calls.len() < max_calls {
let method_name = match parser.next_token::<MethodName>() {
Ok(Token::String(method)) => method,
Ok(_) => {
return Err(trc::JmapEvent::NotRequest
.into_err()
.details("Invalid JMAP request"));
}
Err(err)
if err.matches(trc::EventType::Jmap(
trc::JmapEvent::InvalidArguments,
)) =>
{
MethodName::error()
}
Err(err) => {
return Err(err);
}
};
parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?;
parser.ctx = method_name.obj;
let start_depth_array = parser.depth_array;
let start_depth_dict = parser.depth_dict;
let method = match (&method_name.fnc, &method_name.obj) {
(
MethodFunction::Get,
MethodObject::Email
| MethodObject::Mailbox
| MethodObject::Thread
| MethodObject::Identity
| MethodObject::EmailSubmission
| MethodObject::PushSubscription
| MethodObject::VacationResponse
| MethodObject::SieveScript
| MethodObject::Principal
| MethodObject::Quota
| MethodObject::Blob,
) => GetRequest::parse(parser).map(RequestMethod::Get),
(MethodFunction::Get, MethodObject::SearchSnippet) => {
GetSearchSnippetRequest::parse(parser)
.map(RequestMethod::SearchSnippet)
}
(MethodFunction::Query, _) => {
QueryRequest::parse(parser).map(RequestMethod::Query)
}
(MethodFunction::Set, _) => {
SetRequest::parse(parser).map(RequestMethod::Set)
}
(MethodFunction::Changes, _) => {
ChangesRequest::parse(parser).map(RequestMethod::Changes)
}
(MethodFunction::QueryChanges, _) => {
QueryChangesRequest::parse(parser).map(RequestMethod::QueryChanges)
}
(MethodFunction::Copy, MethodObject::Email) => {
CopyRequest::parse(parser).map(RequestMethod::Copy)
}
(MethodFunction::Copy, MethodObject::Blob) => {
CopyBlobRequest::parse(parser).map(RequestMethod::CopyBlob)
}
(MethodFunction::Lookup, MethodObject::Blob) => {
BlobLookupRequest::parse(parser).map(RequestMethod::LookupBlob)
}
(MethodFunction::Upload, MethodObject::Blob) => {
BlobUploadRequest::parse(parser).map(RequestMethod::UploadBlob)
}
(MethodFunction::Import, MethodObject::Email) => {
ImportEmailRequest::parse(parser).map(RequestMethod::ImportEmail)
}
(MethodFunction::Parse, MethodObject::Email) => {
ParseEmailRequest::parse(parser).map(RequestMethod::ParseEmail)
}
(MethodFunction::Validate, MethodObject::SieveScript) => {
ValidateSieveScriptRequest::parse(parser)
.map(RequestMethod::ValidateScript)
}
(MethodFunction::Echo, MethodObject::Core) => {
Echo::parse(parser).map(RequestMethod::Echo)
}
_ => Err(trc::JmapEvent::UnknownMethod
.into_err()
.details(method_name.to_string())),
};
let method = match method {
Ok(method) => method,
Err(err) if !err.is_jmap_method_error() => {
parser.skip_token(start_depth_array, start_depth_dict)?;
RequestMethod::Error(err)
}
Err(err) => {
return Err(err);
}
};
parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?;
let id = parser.next_token::<String>()?.unwrap_string("")?;
parser
.next_token::<Ignore>()?
.assert_jmap(Token::ArrayEnd)?;
self.method_calls.push(Call {
id,
method,
name: method_name,
});
} else {
return Err(trc::LimitEvent::CallsIn.into_err());
}
}
Ok(true)
}
0x7364_4964_6574_6165_7263 => {
let mut created_ids = HashMap::new();
parser.next_token::<Ignore>()?.assert(Token::DictStart)?;
while let Some(key) = parser.next_dict_key::<String>()? {
created_ids.insert(
key,
parser.next_token::<AnyId>()?.unwrap_string("createdIds")?,
);
}
self.created_ids = Some(created_ids);
Ok(true)
}
_ => {
parser.skip_token(parser.depth_array, parser.depth_dict)?;
Ok(false)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::request::Request;
const TEST: &str = r#"
{
"using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
"methodCalls": [
[ "method1", {
"arg1": "arg1data",
"arg2": "arg2data"
}, "c1" ],
[ "Core/echo", {
"hello": true,
"high": 5
}, "c2" ],
[ "method3", {"hello": [{"a": {"b": true}}]}, "c3" ]
],
"createdIds": {
"c1": "m1",
"c2": "m2"
}
}
"#;
const TEST2: &str = r##"
{
"using": [
"urn:ietf:params:jmap:submission",
"urn:ietf:params:jmap:mail",
"urn:ietf:params:jmap:core"
],
"methodCalls": [
[
"Email/set",
{
"accountId": "c",
"create": {
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
"mailboxIds": {
"9": true
},
"subject": "test",
"from": [
{
"name": "Foo",
"email": "foo@bar.com"
}
],
"to": [
{
"name": null,
"email": "bar@foo.com"
}
],
"cc": [],
"bcc": [],
"replyTo": [
{
"name": null,
"email": "foo@bar.com"
}
],
"htmlBody": [
{
"partId": "c37ee58b-e224-4799-88e6-1d7484e3b782",
"type": "text/html"
}
],
"bodyValues": {
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
"value": "<p>test email<br></p>",
"isEncodingProblem": false,
"isTruncated": false
}
},
"header:User-Agent:asText": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0"
}
}
},
"c0"
],
[
"EmailSubmission/set",
{
"accountId": "c",
"create": {
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
"identityId": "a",
"emailId": "#c37ee58b-e224-4799-88e6-1d7484e3b782",
"envelope": {
"mailFrom": {
"email": "foo@bar.com"
},
"rcptTo": [
{
"email": "bar@foo.com"
}
]
}
}
},
"onSuccessUpdateEmail": {
"#c37ee58b-e224-4799-88e6-1d7484e3b782": {
"mailboxIds/d": true,
"mailboxIds/9": null,
"keywords/$seen": true,
"keywords/$draft": null
}
}
},
"c1"
]
]
}
"##;
#[test]
fn parse_request() {
println!("{:?}", Request::parse(TEST.as_bytes(), 10, 10240));
println!("{:?}", Request::parse(TEST2.as_bytes(), 10, 10240));
}
}