The tenant switch. A tenant's administrator turns legacy mail protocols off for its own tenant, and from then on sign-in over IMAP, POP3, ManageSieve and SMTP AUTH is refused for every address on the tenant's domains, while every other domain on the server carries on. No port closes, since other tenants share them (LP-13): it is one stored fact per tenant, read at sign-in and when client configuration is answered. inbuxa:TenantProtocolPolicy/get and /set, one per tenant, id the tenant's: - Inside a tenant, a principal reaches only its own tenant's switch (MT-1): /get with no ids answers with it, another tenant's is notFound and can't be changed. At server level /get with no ids lists every tenant's. - Turning it off is always allowed. Turning it back on is refused with forbidden, naming inbuxa:ProtocolPolicy, while the server has legacy protocols off (LP-9). - A change raises security.legacy-protocols-changed with policy = tenant, the tenant's id, the new value and who made it (LP-14). - It takes sysDomainGet and sysDomainUpdate, not the two new permissions the spec names. The switch governs sign-in on the tenant's domains, so whoever manages those domains may turn it -- and the default Tenant Administrator role already holds both, where new permissions would reach no role already stored on a server (MT-12's note), leaving today's tenant administrators without the switch until someone edited their role by hand. The same trade inbuxa:AiLimits and inbuxa:ProtocolPolicy made. /query is not built yet; /get with no ids covers listing. Sign-in (LP-10 to LP-12). Before the credentials are looked at, the name given is resolved to its domain and the domain to its tenant, so a real account and a made-up address on the domain get the same refusal, with a right password or a wrong one, counted as no failed sign-in (LP-11). The words are the spec's: "Your organization allows only INBUXA webmail and JMAP apps...", in each protocol's form. A bearer token needn't name an account, so after authentication the account's own tenant is checked too; a token that named nobody can't slip past. The refusal carries policy = tenant and the domain, not the tenant's id: IMAP answers a command's tag from the Id key, so an error holding one was sent under the wrong tag and the mail app hung waiting for its reply. The first live run found that; a unit test now holds the refusal to it. Client configuration (LP-14a). Autoconfig, autodiscover, PACC and the suggested DNS records now ask whether legacy services are off for the domain being answered for -- the server's switch, or the domain's tenant's -- so a tenant's domains stop offering IMAP, POP3 and submission while others still do. tests/e2e/legacy_protocols.py builds a tenant with its own domain, a user and a tenant administrator, and a second tenant, and proves on a running server: the admin sees and changes only its own tenant's switch (test 10); turning it off is an event (test 14); the tenant's user is refused over IMAP with the right password and a wrong one, a made-up address on the domain the same (tests 6, 7); POP3 and submission refuse in their own forms and JMAP still works (test 8); an account on another domain signs in normally (test 6); autoconfig drops IMAP for the tenant's domain only; with the server off, the tenant can't turn it back on (test 9); and once back on, the user signs in again. All 62 checks pass.
1025 lines
44 KiB
Rust
1025 lines
44 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 super::{
|
|
Call, Request, RequestMethod,
|
|
method::{MethodFunction, MethodName, MethodObject},
|
|
};
|
|
use crate::request::{
|
|
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
|
|
QueryRequestMethod, SetRequestMethod, deserialize::DeserializeArguments,
|
|
};
|
|
use serde::{
|
|
Deserialize, Deserializer,
|
|
de::{self, SeqAccess, Visitor},
|
|
};
|
|
use std::fmt::{self, Display};
|
|
|
|
impl<'x> Request<'x> {
|
|
pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
|
|
if json.len() <= max_size {
|
|
match serde_json::from_slice::<Request>(json) {
|
|
Ok(request) => {
|
|
if request.method_calls.len() <= max_calls {
|
|
Ok(request)
|
|
} else {
|
|
Err(trc::LimitEvent::CallsIn.into_err())
|
|
}
|
|
}
|
|
Err(err) => Err(trc::JmapEvent::NotRequest
|
|
.into_err()
|
|
.reason(err.to_string())
|
|
.details(String::from_utf8_lossy(json).into_owned())),
|
|
}
|
|
} else {
|
|
Err(trc::LimitEvent::SizeRequest.into_err())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> DeserializeArguments<'de> for Request<'de> {
|
|
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
|
where
|
|
A: serde::de::MapAccess<'de>,
|
|
{
|
|
hashify::fnc_map!(key.as_bytes(),
|
|
b"using" => {
|
|
self.using = map.next_value()?;
|
|
},
|
|
b"methodCalls" => {
|
|
self.method_calls = map.next_value()?;
|
|
},
|
|
b"createdIds" => {
|
|
self.created_ids = map.next_value()?;
|
|
},
|
|
_ => {
|
|
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
|
}
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct CallVisitor;
|
|
|
|
impl<'de> Visitor<'de> for CallVisitor {
|
|
type Value = Call<RequestMethod<'de>>;
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
formatter.write_str("an array with 3 elements")
|
|
}
|
|
|
|
fn visit_seq<V>(self, mut seq: V) -> Result<Call<RequestMethod<'de>>, V::Error>
|
|
where
|
|
V: SeqAccess<'de>,
|
|
{
|
|
let method_name = seq
|
|
.next_element::<std::borrow::Cow<str>>()?
|
|
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
|
let name = match MethodName::parse(method_name.as_ref()) {
|
|
Some(name) => name,
|
|
None => {
|
|
// Ignore the rest of the call
|
|
let _ = seq
|
|
.next_element::<serde::de::IgnoredAny>()?
|
|
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
|
let id = seq
|
|
.next_element::<String>()?
|
|
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
|
|
|
return Ok(Call {
|
|
id,
|
|
method: RequestMethod::Error(
|
|
trc::JmapEvent::UnknownMethod
|
|
.into_err()
|
|
.details(method_name.to_string()),
|
|
),
|
|
name: MethodName::error(),
|
|
});
|
|
}
|
|
};
|
|
|
|
let method = match (&name.fnc, &name.obj) {
|
|
(MethodFunction::Get, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Email(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Mailbox) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Mailbox(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Thread) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Thread(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Identity) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Identity(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::EmailSubmission) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::EmailSubmission(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::PushSubscription) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::PushSubscription(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::MaskedEmail) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::MaskedEmail(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::DeletedAccount) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::DeletedAccount(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::AiLimits) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AiLimits(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::ProtocolPolicy) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ProtocolPolicy(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::TenantProtocolPolicy) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Get(GetRequestMethod::TenantProtocolPolicy(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::VacationResponse) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::VacationResponse(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::SieveScript) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Sieve(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Principal) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Principal(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Quota) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Quota(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Blob) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Blob(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Calendar) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Calendar(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::CalendarEvent) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::CalendarEvent(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::CalendarEventNotification) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Get(GetRequestMethod::CalendarEventNotification(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::Get, MethodObject::ParticipantIdentity) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ParticipantIdentity(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::AddressBook) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AddressBook(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ContactCard(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::FileNode) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::FileNode(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::ShareNotification) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ShareNotification(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::SearchSnippet) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::SearchSnippet(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Get, MethodObject::Registry(_)) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Registry(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Email(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::Mailbox) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Mailbox(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::Identity) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Identity(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::EmailSubmission) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::EmailSubmission(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::PushSubscription) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::PushSubscription(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::MaskedEmail) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::MaskedEmail(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::DeletedAccount) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::DeletedAccount(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::AiLimits) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::AiLimits(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::ProtocolPolicy) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ProtocolPolicy(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::TenantProtocolPolicy) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Set(SetRequestMethod::TenantProtocolPolicy(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::VacationResponse) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::VacationResponse(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::SieveScript) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Sieve(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::Calendar) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Calendar(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::CalendarEvent) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::CalendarEvent(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::CalendarEventNotification) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Set(SetRequestMethod::CalendarEventNotification(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::Set, MethodObject::ParticipantIdentity) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ParticipantIdentity(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::AddressBook) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::AddressBook(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ContactCard(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::FileNode) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::FileNode(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::ShareNotification) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ShareNotification(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Set, MethodObject::Registry(_)) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Registry(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Email(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Mailbox) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Mailbox(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::EmailSubmission) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::EmailSubmission(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::SieveScript) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Sieve(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Principal) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Principal(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Quota) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Quota(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Calendar) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Calendar(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::CalendarEvent) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::CalendarEvent(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::CalendarEventNotification) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Query(QueryRequestMethod::CalendarEventNotification(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::Query, MethodObject::AddressBook) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::AddressBook(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::ContactCard(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::FileNode) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::FileNode(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::ShareNotification) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Query(QueryRequestMethod::ShareNotification(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Query, MethodObject::Registry(_)) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Registry(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::Email(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::Mailbox) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::Mailbox(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::EmailSubmission) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::QueryChanges(
|
|
QueryChangesRequestMethod::EmailSubmission(value),
|
|
),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::QueryChanges, MethodObject::Principal) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::Principal(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::Quota) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::Quota(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::CalendarEvent) => match seq.next_element()
|
|
{
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::CalendarEvent(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::CalendarEventNotification) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::QueryChanges(
|
|
QueryChangesRequestMethod::CalendarEventNotification(value),
|
|
),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::QueryChanges, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::ContactCard(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::FileNode) => match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::QueryChanges(QueryChangesRequestMethod::FileNode(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::QueryChanges, MethodObject::ShareNotification) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::QueryChanges(
|
|
QueryChangesRequestMethod::ShareNotification(value),
|
|
),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::Changes, _) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Changes(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Copy, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::Email(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Copy, MethodObject::Blob) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::Blob(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Copy, MethodObject::CalendarEvent) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::CalendarEvent(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Copy, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::ContactCard(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Copy, MethodObject::FileNode) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::FileNode(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Lookup, MethodObject::Blob) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::LookupBlob(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Upload, MethodObject::Blob) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::UploadBlob(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Import, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::ImportEmail(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Parse, MethodObject::Email) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::Email(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Parse, MethodObject::CalendarEvent) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::CalendarEvent(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Parse, MethodObject::ContactCard) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::ContactCard(value)),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::GetAvailability, MethodObject::Principal) => {
|
|
match seq.next_element() {
|
|
Ok(Some(value)) => {
|
|
RequestMethod::Get(GetRequestMethod::PrincipalAvailability(value))
|
|
}
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
}
|
|
}
|
|
(MethodFunction::Validate, MethodObject::SieveScript) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::ValidateScript(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
(MethodFunction::Echo, MethodObject::Core) => match seq.next_element() {
|
|
Ok(Some(value)) => RequestMethod::Echo(value),
|
|
Err(err) => RequestMethod::invalid(err),
|
|
Ok(None) => {
|
|
return Err(de::Error::invalid_length(1, &self));
|
|
}
|
|
},
|
|
_ => {
|
|
return Err(de::Error::custom(format!(
|
|
"Invalid method function/object combination: {}",
|
|
method_name
|
|
)));
|
|
}
|
|
};
|
|
|
|
let id = seq
|
|
.next_element::<String>()?
|
|
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
|
|
|
Ok(Call { id, method, name })
|
|
}
|
|
}
|
|
|
|
impl RequestMethod<'_> {
|
|
fn invalid(err: impl Display) -> Self {
|
|
RequestMethod::Error(
|
|
trc::JmapEvent::InvalidArguments
|
|
.into_err()
|
|
.details(err.to_string()),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Request<'de> {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
struct RequestVisitor;
|
|
|
|
impl<'de> Visitor<'de> for RequestVisitor {
|
|
type Value = Request<'de>;
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
formatter.write_str("a JMAP request object")
|
|
}
|
|
|
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
where
|
|
A: de::MapAccess<'de>,
|
|
{
|
|
let mut target = Request::default();
|
|
let mut has_using = false;
|
|
let mut has_method_calls = false;
|
|
|
|
while let Some(key) = map.next_key::<&str>()? {
|
|
match key {
|
|
"using" => has_using = true,
|
|
"methodCalls" => has_method_calls = true,
|
|
_ => {}
|
|
}
|
|
target
|
|
.deserialize_argument(key, &mut map)
|
|
.map_err(de::Error::custom)?;
|
|
}
|
|
|
|
if !has_using || !has_method_calls {
|
|
return Err(de::Error::custom(
|
|
"Request is missing the \"using\" or \"methodCalls\" property.",
|
|
));
|
|
}
|
|
|
|
Ok(target)
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_map(RequestVisitor)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Call<RequestMethod<'de>> {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
deserializer.deserialize_seq(CallVisitor)
|
|
}
|
|
}
|
|
|
|
#[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 TEST1: &str = r#"
|
|
{
|
|
"using": [
|
|
"urn:ietf:params:jmap:core",
|
|
"urn:ietf:params:jmap:mail"
|
|
],
|
|
"methodCalls": [
|
|
[
|
|
"Email/query",
|
|
{
|
|
"accountId": "0",
|
|
"filter": { "conditions": [ { "hasKeyword": "music", "maxSize": 455 }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" },
|
|
"sort": [
|
|
{
|
|
"property": "subject",
|
|
"isAscending": true
|
|
},
|
|
{
|
|
"property": "allInThreadHaveKeyword",
|
|
"isAscending": false,
|
|
"keyword": "$seen"
|
|
},
|
|
{
|
|
"keyword": "$junk",
|
|
"property": "someInThreadHaveKeyword",
|
|
"collation": "i;octet",
|
|
"isAscending": false
|
|
}
|
|
],
|
|
"position": 0,
|
|
"limit": 10
|
|
},
|
|
"c1"
|
|
]
|
|
],
|
|
"createdIds": {}
|
|
}
|
|
"#;
|
|
|
|
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": "[email protected]"
|
|
}
|
|
],
|
|
"to": [
|
|
{
|
|
"name": null,
|
|
"email": "[email protected]"
|
|
}
|
|
],
|
|
"cc": [],
|
|
"bcc": [],
|
|
"replyTo": [
|
|
{
|
|
"name": null,
|
|
"email": "[email protected]"
|
|
}
|
|
],
|
|
"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": "[email protected]"
|
|
},
|
|
"rcptTo": [
|
|
{
|
|
"email": "[email protected]"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"onSuccessUpdateEmail": {
|
|
"#c37ee58b-e224-4799-88e6-1d7484e3b782": {
|
|
"mailboxIds/d": true,
|
|
"mailboxIds/9": null,
|
|
"keywords/$seen": true,
|
|
"keywords/$draft": null
|
|
}
|
|
}
|
|
},
|
|
"c1"
|
|
]
|
|
]
|
|
}
|
|
"##;
|
|
|
|
const TEST_ESCAPED_SOLIDUS: &str = r#"
|
|
{
|
|
"using": [ "urn:ietf:params:jmap:core" ],
|
|
"methodCalls": [
|
|
[ "Core\/echo", { "hello": true }, "c1" ]
|
|
]
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn parse_request() {
|
|
println!("{:#?}", Request::parse(TEST.as_bytes(), 10, 10240));
|
|
println!("{:#?}", Request::parse(TEST1.as_bytes(), 10, 10240));
|
|
println!("{:#?}", Request::parse(TEST2.as_bytes(), 10, 10240));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_method_name_with_escaped_solidus() {
|
|
let request = Request::parse(TEST_ESCAPED_SOLIDUS.as_bytes(), 10, 10240)
|
|
.expect("escaped solidus in method name must parse");
|
|
assert_eq!(request.method_calls.len(), 1);
|
|
assert!(matches!(
|
|
request.method_calls[0].method,
|
|
super::RequestMethod::Echo(_)
|
|
));
|
|
}
|
|
}
|