pub use jmap_types::{GetObject, JmapObject, QueryObject, SetObject};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetError {
#[serde(rename = "type")]
pub error_type: SetErrorType,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<String>>,
#[serde(rename = "existingId", skip_serializing_if = "Option::is_none")]
pub existing_id: Option<jmap_types::Id>,
#[serde(rename = "maxRecipients", skip_serializing_if = "Option::is_none")]
pub max_recipients: Option<u64>,
#[serde(rename = "invalidRecipients", skip_serializing_if = "Option::is_none")]
pub invalid_recipients: Option<Vec<String>>,
#[serde(rename = "notFound", skip_serializing_if = "Option::is_none")]
pub not_found: Option<Vec<jmap_types::Id>>,
#[serde(rename = "maxSize", skip_serializing_if = "Option::is_none")]
pub max_size: Option<u64>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl SetError {
pub fn new(error_type: SetErrorType) -> Self {
Self {
error_type,
description: None,
properties: None,
existing_id: None,
max_recipients: None,
invalid_recipients: None,
not_found: None,
max_size: None,
extra: serde_json::Map::new(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_properties<I, S>(mut self, props: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.properties = Some(props.into_iter().map(|s| s.into()).collect());
self
}
pub fn with_existing_id(mut self, id: jmap_types::Id) -> Self {
self.existing_id = Some(id);
self
}
pub fn with_max_recipients(mut self, n: u64) -> Self {
self.max_recipients = Some(n);
self
}
pub fn with_invalid_recipients<I, S>(mut self, addrs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.invalid_recipients = Some(addrs.into_iter().map(|s| s.into()).collect());
self
}
pub fn with_not_found(mut self, ids: Vec<jmap_types::Id>) -> Self {
self.not_found = Some(ids);
self
}
pub fn with_max_size(mut self, n: u64) -> Self {
self.max_size = Some(n);
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
let key: String = key.into();
debug_assert!(
!RESERVED_SET_ERROR_WIRE_NAMES.contains(&key.as_str()),
"SetError::with_extra called with reserved wire-name key {key:?} \
— would produce a malformed JSON SetError on the wire. \
Choose an extension-namespace key that does not collide \
with the typed-field wire names \
({RESERVED_SET_ERROR_WIRE_NAMES:?})."
);
self.extra.insert(key, value);
self
}
pub fn validate_extras(&self) -> Result<(), ReservedExtrasKey> {
for key in self.extra.keys() {
if RESERVED_SET_ERROR_WIRE_NAMES.contains(&key.as_str()) {
return Err(ReservedExtrasKey { key: key.clone() });
}
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReservedExtrasKey {
pub key: String,
}
impl std::fmt::Display for ReservedExtrasKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SetError.extra contains reserved wire-name key {:?} — would \
produce a malformed JSON SetError on the wire",
self.key
)
}
}
impl std::error::Error for ReservedExtrasKey {}
pub const RESERVED_SET_ERROR_WIRE_NAMES: &[&str] = &[
"type",
"description",
"properties",
"existingId",
"maxRecipients",
"invalidRecipients",
"notFound",
"maxSize",
];
impl std::fmt::Display for SetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error_type)?;
if let Some(ref desc) = self.description {
write!(f, ": {desc}")?;
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum SetErrorType {
Forbidden,
OverQuota,
TooLarge,
RateLimit,
NotFound,
InvalidPatch,
WillDestroy,
InvalidProperties,
Singleton,
AlreadyExists,
MailboxHasChild,
MailboxHasEmail,
TooManyKeywords,
TooManyMailboxes,
BlobNotFound,
ForbiddenFrom,
InvalidEmail,
TooManyRecipients,
NoRecipients,
InvalidRecipients,
ForbiddenMailFrom,
ForbiddenToSend,
CannotUnsend,
Custom(String),
}
impl SetErrorType {
pub fn custom(s: impl Into<String>) -> Self {
let s: String = s.into();
Self::from_wire_str(&s).unwrap_or(Self::Custom(s))
}
fn from_wire_str(s: &str) -> Option<Self> {
Some(match s {
"forbidden" => Self::Forbidden,
"overQuota" => Self::OverQuota,
"tooLarge" => Self::TooLarge,
"rateLimit" => Self::RateLimit,
"notFound" => Self::NotFound,
"invalidPatch" => Self::InvalidPatch,
"willDestroy" => Self::WillDestroy,
"invalidProperties" => Self::InvalidProperties,
"singleton" => Self::Singleton,
"alreadyExists" => Self::AlreadyExists,
"mailboxHasChild" => Self::MailboxHasChild,
"mailboxHasEmail" => Self::MailboxHasEmail,
"tooManyKeywords" => Self::TooManyKeywords,
"tooManyMailboxes" => Self::TooManyMailboxes,
"blobNotFound" => Self::BlobNotFound,
"forbiddenFrom" => Self::ForbiddenFrom,
"invalidEmail" => Self::InvalidEmail,
"tooManyRecipients" => Self::TooManyRecipients,
"noRecipients" => Self::NoRecipients,
"invalidRecipients" => Self::InvalidRecipients,
"forbiddenMailFrom" => Self::ForbiddenMailFrom,
"forbiddenToSend" => Self::ForbiddenToSend,
"cannotUnsend" => Self::CannotUnsend,
_ => return None,
})
}
}
impl std::fmt::Display for SetErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s: &str = match self {
Self::Forbidden => "forbidden",
Self::OverQuota => "overQuota",
Self::TooLarge => "tooLarge",
Self::RateLimit => "rateLimit",
Self::NotFound => "notFound",
Self::InvalidPatch => "invalidPatch",
Self::WillDestroy => "willDestroy",
Self::InvalidProperties => "invalidProperties",
Self::Singleton => "singleton",
Self::AlreadyExists => "alreadyExists",
Self::MailboxHasChild => "mailboxHasChild",
Self::MailboxHasEmail => "mailboxHasEmail",
Self::TooManyKeywords => "tooManyKeywords",
Self::TooManyMailboxes => "tooManyMailboxes",
Self::BlobNotFound => "blobNotFound",
Self::ForbiddenFrom => "forbiddenFrom",
Self::InvalidEmail => "invalidEmail",
Self::TooManyRecipients => "tooManyRecipients",
Self::NoRecipients => "noRecipients",
Self::InvalidRecipients => "invalidRecipients",
Self::ForbiddenMailFrom => "forbiddenMailFrom",
Self::ForbiddenToSend => "forbiddenToSend",
Self::CannotUnsend => "cannotUnsend",
Self::Custom(s) => s.as_str(),
};
f.write_str(s)
}
}
impl serde::Serialize for SetErrorType {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for SetErrorType {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = SetErrorType;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a JMAP SetError type string")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(SetErrorType::from_wire_str(v)
.unwrap_or_else(|| SetErrorType::Custom(v.to_owned())))
}
}
d.deserialize_str(Visitor)
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BackendSetError<E> {
SetError(SetError),
Other(E),
}
impl<E: std::fmt::Display> std::fmt::Display for BackendSetError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetError(se) => write!(f, "set error: {se}"),
Self::Other(e) => write!(f, "{e}"),
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for BackendSetError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Other(e) => Some(e),
_ => None,
}
}
}
impl<E> From<SetError> for BackendSetError<E> {
fn from(e: SetError) -> Self {
Self::SetError(e)
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BackendChangesError<E> {
CannotCalculate,
TooManyChanges {
limit: u64,
},
Other(E),
}
impl<E: std::fmt::Display> std::fmt::Display for BackendChangesError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CannotCalculate => write!(f, "cannot calculate changes"),
Self::TooManyChanges { limit: 0 } => write!(f, "cannot calculate changes"),
Self::TooManyChanges { limit } => write!(f, "too many changes (limit: {limit})"),
Self::Other(e) => write!(f, "{e}"),
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for BackendChangesError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Other(e) => Some(e),
_ => None,
}
}
}
impl<E> From<E> for BackendChangesError<E> {
fn from(e: E) -> Self {
Self::Other(e)
}
}
impl<E: std::error::Error> From<BackendChangesError<E>> for jmap_types::JmapError {
fn from(e: BackendChangesError<E>) -> Self {
match e {
BackendChangesError::CannotCalculate => {
jmap_types::JmapError::cannot_calculate_changes()
}
BackendChangesError::TooManyChanges { limit: 0 } => {
jmap_types::JmapError::cannot_calculate_changes()
}
BackendChangesError::TooManyChanges { limit } => {
jmap_types::JmapError::too_many_changes_with_limit(limit)
}
BackendChangesError::Other(_inner) => {
jmap_types::JmapError::server_fail(crate::handlers::SERVER_FAIL_INTERNAL_DESC)
}
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ChangesResult {
pub created: Vec<jmap_types::Id>,
pub updated: Vec<jmap_types::Id>,
pub destroyed: Vec<jmap_types::Id>,
pub has_more_changes: bool,
pub new_state: jmap_types::State,
}
impl ChangesResult {
pub fn new(
created: Vec<jmap_types::Id>,
updated: Vec<jmap_types::Id>,
destroyed: Vec<jmap_types::Id>,
has_more_changes: bool,
new_state: jmap_types::State,
) -> Self {
Self {
created,
updated,
destroyed,
has_more_changes,
new_state,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct QueryResult {
pub ids: Vec<jmap_types::Id>,
pub position: u64,
pub total: Option<u64>,
pub query_state: jmap_types::State,
pub can_calculate_changes: bool,
}
impl QueryResult {
pub fn new(
ids: Vec<jmap_types::Id>,
position: u64,
total: Option<u64>,
query_state: jmap_types::State,
can_calculate_changes: bool,
) -> Self {
Self {
ids,
position,
total,
query_state,
can_calculate_changes,
}
}
pub fn new_clamped(
ids: Vec<jmap_types::Id>,
position_signed: i64,
total: Option<u64>,
query_state: jmap_types::State,
can_calculate_changes: bool,
) -> Self {
let position = u64::try_from(position_signed.max(0)).unwrap_or(0);
Self::new(ids, position, total, query_state, can_calculate_changes)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct AddedItem {
pub id: jmap_types::Id,
pub index: u64,
}
impl AddedItem {
pub fn new(id: jmap_types::Id, index: u64) -> Self {
Self { id, index }
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct QueryChangesResult {
pub old_query_state: jmap_types::State,
pub new_query_state: jmap_types::State,
pub total: Option<u64>,
pub removed: Vec<jmap_types::Id>,
pub added: Vec<AddedItem>,
}
impl QueryChangesResult {
pub fn new(
old_query_state: jmap_types::State,
new_query_state: jmap_types::State,
total: Option<u64>,
removed: Vec<jmap_types::Id>,
added: Vec<AddedItem>,
) -> Self {
Self {
old_query_state,
new_query_state,
total,
removed,
added,
}
}
}
pub trait JmapBackend: Send + Sync + 'static {
type Error: std::error::Error + Send + Sync + 'static;
type CallerCtx: Clone + Send + Sync + 'static;
fn account_exists(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
) -> impl std::future::Future<Output = Result<bool, Self::Error>> + Send;
fn get_objects<O: GetObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
ids: Option<&[jmap_types::Id]>,
properties: Option<&[String]>,
) -> impl std::future::Future<Output = Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error>> + Send;
fn get_state<O: JmapObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
) -> impl std::future::Future<Output = Result<jmap_types::State, Self::Error>> + Send;
fn get_changes<O: JmapObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
since_state: &jmap_types::State,
max_changes: Option<u64>,
) -> impl std::future::Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send;
#[allow(clippy::too_many_arguments)]
fn query_objects<O: QueryObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
filter: Option<&O::Filter>,
sort: Option<&[O::Comparator]>,
limit: Option<u64>,
position: i64,
) -> impl std::future::Future<Output = Result<QueryResult, Self::Error>> + Send;
#[allow(clippy::too_many_arguments)]
fn query_changes<O: QueryObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &jmap_types::Id,
since_query_state: &jmap_types::State,
filter: Option<&O::Filter>,
sort: Option<&[O::Comparator]>,
max_changes: Option<u64>,
up_to_id: Option<&jmap_types::Id>,
collapse_threads: bool,
) -> impl std::future::Future<
Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>,
> + Send;
fn principal_id(caller: &Self::CallerCtx) -> Option<&jmap_types::Id> {
let _ = caller;
None
}
fn max_objects_in_set(&self, caller: &Self::CallerCtx, account_id: &jmap_types::Id) -> u64 {
let _ = (caller, account_id);
500
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_changes_error_limit_zero_maps_to_cannot_calculate() {
let err = jmap_types::JmapError::from(
BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 0 },
);
assert_eq!(
err.error_type.as_str(),
"cannotCalculateChanges",
"limit=0 must produce cannotCalculateChanges; got: {:?}",
err.error_type
);
}
#[test]
fn backend_changes_error_cannot_calculate_maps_to_cannot_calculate_changes() {
let err = jmap_types::JmapError::from(
BackendChangesError::<std::convert::Infallible>::CannotCalculate,
);
assert_eq!(
err.error_type.as_str(),
"cannotCalculateChanges",
"CannotCalculate must produce cannotCalculateChanges; got: {:?}",
err.error_type
);
let s = BackendChangesError::<std::convert::Infallible>::CannotCalculate.to_string();
assert_eq!(
s, "cannot calculate changes",
"Display must produce the same string as TooManyChanges {{ limit: 0 }}"
);
}
#[test]
fn backend_changes_error_nonzero_limit_maps_to_too_many_changes() {
let err = jmap_types::JmapError::from(
BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 50 },
);
assert_eq!(
err.error_type.as_str(),
"tooManyChanges",
"limit=50 must produce tooManyChanges; got: {:?}",
err.error_type
);
}
#[test]
fn backend_changes_error_other_drops_display_text() {
#[derive(Debug)]
struct LeakyError(&'static str);
impl std::fmt::Display for LeakyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for LeakyError {}
const CANARY: &str = "TOKEN-DO-NOT-LEAK-c0ffee";
let err: BackendChangesError<LeakyError> = BackendChangesError::Other(LeakyError(CANARY));
let jmap_err = jmap_types::JmapError::from(err);
let wire = serde_json::to_value(&jmap_err).expect("JmapError must serialize");
let wire_str = wire.to_string();
assert!(
!wire_str.contains(CANARY),
"From<BackendChangesError<E>> for JmapError must not echo \
backend error Display onto the wire; got {wire_str}"
);
assert_eq!(
wire["description"],
crate::handlers::SERVER_FAIL_INTERNAL_DESC,
"description must be the static 'internal error' string"
);
assert_eq!(wire["type"], "serverFail");
}
#[test]
fn custom_canonicalises_known_wire_names_to_typed_variants() {
let cases: &[(&str, SetErrorType)] = &[
("forbidden", SetErrorType::Forbidden),
("overQuota", SetErrorType::OverQuota),
("invalidPatch", SetErrorType::InvalidPatch),
("mailboxHasChild", SetErrorType::MailboxHasChild),
("tooManyRecipients", SetErrorType::TooManyRecipients),
("cannotUnsend", SetErrorType::CannotUnsend),
];
for (name, expected) in cases {
let from_custom = SetErrorType::custom(*name);
assert_eq!(
&from_custom, expected,
"custom({name:?}) must canonicalise to the typed variant, not Custom"
);
assert!(
!matches!(from_custom, SetErrorType::Custom(_)),
"custom({name:?}) must NOT remain Custom — known wire-name asymmetry"
);
}
let unknown = SetErrorType::custom("mdnAlreadySent");
assert!(
matches!(unknown, SetErrorType::Custom(ref s) if s == "mdnAlreadySent"),
"custom('mdnAlreadySent') must remain Custom (not a known wire-name)"
);
}
#[test]
fn set_error_type_custom_round_trips_as_bare_string() {
let original = SetErrorType::custom("mdnAlreadySent");
let serialized = serde_json::to_string(&original).expect("serialize");
assert_eq!(
serialized, r#""mdnAlreadySent""#,
"Custom must serialize as bare string"
);
let deserialized: SetErrorType = serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(
deserialized, original,
"Custom must deserialize back to Custom"
);
}
#[test]
fn set_error_extra_field_round_trips() {
let original = SetError::new(SetErrorType::custom("rateLimited"))
.with_description("Slow mode is active")
.with_extra(
"serverRetryAfter",
serde_json::Value::String("2025-12-31T23:59:59Z".to_owned()),
);
let wire = serde_json::to_value(&original).expect("serialize");
assert_eq!(wire["type"], "rateLimited");
assert_eq!(wire["description"], "Slow mode is active");
assert_eq!(
wire["serverRetryAfter"], "2025-12-31T23:59:59Z",
"extra field must flatten into the SetError wire shape"
);
let round: SetError = serde_json::from_value(wire).expect("deserialize");
assert_eq!(round.error_type, original.error_type);
assert_eq!(round.description, original.description);
assert_eq!(
round.extra.get("serverRetryAfter").and_then(|v| v.as_str()),
Some("2025-12-31T23:59:59Z"),
"extra field must survive deserialize"
);
}
#[test]
fn set_error_empty_extra_is_invisible_on_the_wire() {
let err = SetError::new(SetErrorType::Forbidden);
let wire = serde_json::to_value(&err).expect("serialize");
let obj = wire.as_object().expect("object");
assert!(
!obj.contains_key("extra"),
"empty `extra` map must not appear on the wire (got {wire})"
);
assert_eq!(
obj.len(),
1,
"bare SetError must have exactly one key on the wire"
);
assert_eq!(obj["type"], "forbidden");
}
#[test]
fn set_error_unknown_field_lands_in_extra() {
let wire = serde_json::json!({
"type": "forbidden",
"futureSpecField": "future-value",
"anotherOne": 42
});
let err: SetError = serde_json::from_value(wire).expect("deserialize");
assert_eq!(err.error_type, SetErrorType::Forbidden);
assert_eq!(
err.extra.get("futureSpecField").and_then(|v| v.as_str()),
Some("future-value")
);
assert_eq!(
err.extra.get("anotherOne").and_then(|v| v.as_u64()),
Some(42)
);
}
#[test]
#[cfg(debug_assertions)]
fn with_extra_panics_on_reserved_wire_name() {
for &reserved in RESERVED_SET_ERROR_WIRE_NAMES {
let reserved_owned = reserved.to_owned();
let result = std::panic::catch_unwind(move || {
SetError::new(SetErrorType::Forbidden)
.with_extra(&reserved_owned, serde_json::Value::Null);
});
assert!(
result.is_err(),
"with_extra({reserved:?}, ...) must panic in debug builds; \
reserved wire-names collide with typed fields and would \
produce a malformed SetError on the wire"
);
}
}
#[test]
fn validate_extras_detects_reserved_key_planted_via_direct_mutation() {
for &reserved in RESERVED_SET_ERROR_WIRE_NAMES {
let mut err = SetError::new(SetErrorType::Forbidden);
err.extra
.insert(reserved.to_owned(), serde_json::Value::Null);
let collision = err
.validate_extras()
.expect_err("reserved-name extras key must be detected");
assert_eq!(
collision.key, reserved,
"validate_extras must report the colliding key verbatim"
);
}
}
#[test]
fn validate_extras_accepts_extension_namespace_keys() {
let mut err = SetError::new(SetErrorType::custom("rateLimited"));
err.extra.insert(
"serverRetryAfter".to_owned(),
serde_json::Value::String("2025-12-31T23:59:59Z".to_owned()),
);
err.extra
.insert("retryAttempt".to_owned(), serde_json::Value::from(3));
err.validate_extras()
.expect("extension-namespace keys must pass validation");
}
#[test]
fn with_extra_accepts_extension_namespace_key() {
let err = SetError::new(SetErrorType::custom("rateLimited")).with_extra(
"serverRetryAfter",
serde_json::Value::String("2025-12-31T23:59:59Z".to_owned()),
);
assert_eq!(
err.extra.get("serverRetryAfter").and_then(|v| v.as_str()),
Some("2025-12-31T23:59:59Z"),
"extension-namespace key must land in the extra map"
);
}
#[test]
fn reserved_set_error_wire_names_matches_serialized_surface() {
let err = SetError::new(SetErrorType::Forbidden)
.with_description("desc")
.with_properties(["p1"])
.with_existing_id(jmap_types::Id::from("eid"))
.with_max_recipients(10)
.with_invalid_recipients(["bad@example"])
.with_not_found(vec![jmap_types::Id::from("nfid")])
.with_max_size(1024);
let wire = serde_json::to_value(&err).expect("serialize");
let obj = wire.as_object().expect("SetError must serialize as object");
for key in obj.keys() {
assert!(
RESERVED_SET_ERROR_WIRE_NAMES.contains(&key.as_str()),
"wire-name {key:?} appears on the SetError surface but is \
not in RESERVED_SET_ERROR_WIRE_NAMES — adding a typed \
field to SetError requires extending the constant"
);
}
}
#[test]
fn set_error_type_all_known_variants_round_trip() {
let cases: &[(SetErrorType, &str)] = &[
(SetErrorType::Forbidden, "forbidden"),
(SetErrorType::OverQuota, "overQuota"),
(SetErrorType::TooLarge, "tooLarge"),
(SetErrorType::RateLimit, "rateLimit"),
(SetErrorType::NotFound, "notFound"),
(SetErrorType::InvalidPatch, "invalidPatch"),
(SetErrorType::WillDestroy, "willDestroy"),
(SetErrorType::InvalidProperties, "invalidProperties"),
(SetErrorType::Singleton, "singleton"),
(SetErrorType::AlreadyExists, "alreadyExists"),
(SetErrorType::MailboxHasChild, "mailboxHasChild"),
(SetErrorType::MailboxHasEmail, "mailboxHasEmail"),
(SetErrorType::TooManyKeywords, "tooManyKeywords"),
(SetErrorType::TooManyMailboxes, "tooManyMailboxes"),
(SetErrorType::BlobNotFound, "blobNotFound"),
(SetErrorType::ForbiddenFrom, "forbiddenFrom"),
(SetErrorType::InvalidEmail, "invalidEmail"),
(SetErrorType::TooManyRecipients, "tooManyRecipients"),
(SetErrorType::NoRecipients, "noRecipients"),
(SetErrorType::InvalidRecipients, "invalidRecipients"),
(SetErrorType::ForbiddenMailFrom, "forbiddenMailFrom"),
(SetErrorType::ForbiddenToSend, "forbiddenToSend"),
(SetErrorType::CannotUnsend, "cannotUnsend"),
];
for (variant, expected_wire) in cases {
assert_eq!(
variant.to_string(),
*expected_wire,
"Display arm for {variant:?} produced wrong wire string"
);
let serialized = serde_json::to_string(variant).expect("serialize");
assert_eq!(
serialized,
format!("\"{expected_wire}\""),
"Serialize for {variant:?} did not produce \"{expected_wire}\""
);
let deserialized: SetErrorType =
serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(
&deserialized, variant,
"Deserialize of {expected_wire:?} did not rebuild {variant:?} \
(likely fell through to Custom — Display and Deserialize \
match arms have drifted)"
);
assert!(
!matches!(deserialized, SetErrorType::Custom(_)),
"Deserialize of {expected_wire:?} fell through to Custom; \
Display has an arm but Deserialize visitor doesn't"
);
}
}
#[test]
fn principal_id_default_impl_returns_none_for_unit_caller_ctx() {
struct StubBackend;
#[derive(Debug)]
struct StubError;
impl std::fmt::Display for StubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("stub")
}
}
impl std::error::Error for StubError {}
impl JmapBackend for StubBackend {
type Error = StubError;
type CallerCtx = ();
async fn account_exists(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
) -> Result<bool, Self::Error> {
unreachable!("only principal_id is exercised in this test")
}
async fn get_objects<O: GetObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_ids: Option<&[jmap_types::Id]>,
_properties: Option<&[String]>,
) -> Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error> {
unreachable!("only principal_id is exercised in this test")
}
async fn get_state<O: JmapObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
) -> Result<jmap_types::State, Self::Error> {
unreachable!("only principal_id is exercised in this test")
}
async fn get_changes<O: JmapObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_since_state: &jmap_types::State,
_max_changes: Option<u64>,
) -> Result<ChangesResult, BackendChangesError<Self::Error>> {
unreachable!("only principal_id is exercised in this test")
}
async fn query_objects<O: QueryObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_filter: Option<&O::Filter>,
_sort: Option<&[O::Comparator]>,
_limit: Option<u64>,
_position: i64,
) -> Result<QueryResult, Self::Error> {
unreachable!("only principal_id is exercised in this test")
}
async fn query_changes<O: QueryObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_since_query_state: &jmap_types::State,
_filter: Option<&O::Filter>,
_sort: Option<&[O::Comparator]>,
_max_changes: Option<u64>,
_up_to_id: Option<&jmap_types::Id>,
_collapse_threads: bool,
) -> Result<QueryChangesResult, BackendChangesError<Self::Error>> {
unreachable!("only principal_id is exercised in this test")
}
}
let caller: <StubBackend as JmapBackend>::CallerCtx = ();
let id = <StubBackend as JmapBackend>::principal_id(&caller);
assert!(
id.is_none(),
"default principal_id impl must return None; got Some({:?})",
id
);
}
#[test]
fn max_objects_in_set_default_impl_returns_500() {
struct StubBackend;
#[derive(Debug)]
struct StubError;
impl std::fmt::Display for StubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("stub")
}
}
impl std::error::Error for StubError {}
impl JmapBackend for StubBackend {
type Error = StubError;
type CallerCtx = ();
async fn account_exists(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
) -> Result<bool, Self::Error> {
unreachable!("only max_objects_in_set is exercised in this test")
}
async fn get_objects<O: GetObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_ids: Option<&[jmap_types::Id]>,
_properties: Option<&[String]>,
) -> Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error> {
unreachable!("only max_objects_in_set is exercised in this test")
}
async fn get_state<O: JmapObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
) -> Result<jmap_types::State, Self::Error> {
unreachable!("only max_objects_in_set is exercised in this test")
}
async fn get_changes<O: JmapObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_since_state: &jmap_types::State,
_max_changes: Option<u64>,
) -> Result<ChangesResult, BackendChangesError<Self::Error>> {
unreachable!("only max_objects_in_set is exercised in this test")
}
async fn query_objects<O: QueryObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_filter: Option<&O::Filter>,
_sort: Option<&[O::Comparator]>,
_limit: Option<u64>,
_position: i64,
) -> Result<QueryResult, Self::Error> {
unreachable!("only max_objects_in_set is exercised in this test")
}
async fn query_changes<O: QueryObject + Send + Sync>(
&self,
_caller: &(),
_account_id: &jmap_types::Id,
_since_query_state: &jmap_types::State,
_filter: Option<&O::Filter>,
_sort: Option<&[O::Comparator]>,
_max_changes: Option<u64>,
_up_to_id: Option<&jmap_types::Id>,
_collapse_threads: bool,
) -> Result<QueryChangesResult, BackendChangesError<Self::Error>> {
unreachable!("only max_objects_in_set is exercised in this test")
}
}
let backend = StubBackend;
let caller: <StubBackend as JmapBackend>::CallerCtx = ();
let id = jmap_types::Id::from("any-account");
assert_eq!(
backend.max_objects_in_set(&caller, &id),
500,
"default max_objects_in_set must return 500"
);
}
}