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: &str, value: serde_json::Value) -> Self {
self.extra.insert(key.to_owned(), value);
self
}
}
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 {
Self::Custom(s.into())
}
}
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.serialize_str(&self.to_string())
}
}
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(match v {
"forbidden" => SetErrorType::Forbidden,
"overQuota" => SetErrorType::OverQuota,
"tooLarge" => SetErrorType::TooLarge,
"rateLimit" => SetErrorType::RateLimit,
"notFound" => SetErrorType::NotFound,
"invalidPatch" => SetErrorType::InvalidPatch,
"willDestroy" => SetErrorType::WillDestroy,
"invalidProperties" => SetErrorType::InvalidProperties,
"singleton" => SetErrorType::Singleton,
"alreadyExists" => SetErrorType::AlreadyExists,
"mailboxHasChild" => SetErrorType::MailboxHasChild,
"mailboxHasEmail" => SetErrorType::MailboxHasEmail,
"tooManyKeywords" => SetErrorType::TooManyKeywords,
"tooManyMailboxes" => SetErrorType::TooManyMailboxes,
"blobNotFound" => SetErrorType::BlobNotFound,
"forbiddenFrom" => SetErrorType::ForbiddenFrom,
"invalidEmail" => SetErrorType::InvalidEmail,
"tooManyRecipients" => SetErrorType::TooManyRecipients,
"noRecipients" => SetErrorType::NoRecipients,
"invalidRecipients" => SetErrorType::InvalidRecipients,
"forbiddenMailFrom" => SetErrorType::ForbiddenMailFrom,
"forbiddenToSend" => SetErrorType::ForbiddenToSend,
"cannotUnsend" => SetErrorType::CannotUnsend,
other => SetErrorType::Custom(other.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> {
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::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::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(inner.to_string())
}
}
}
}
#[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: i64,
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: i64,
total: Option<u64>,
query_state: jmap_types::State,
can_calculate_changes: bool,
) -> Self {
Self {
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
}
}
#[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_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 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]
fn set_error_type_known_variant_round_trips() {
let original = SetErrorType::Singleton;
let serialized = serde_json::to_string(&original).expect("serialize");
assert_eq!(
serialized, r#""singleton""#,
"Singleton must serialize as \"singleton\""
);
let deserialized: SetErrorType = serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(
deserialized, original,
"Singleton must deserialize back to Singleton"
);
}
#[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
);
}
}