jmap: read null /set outcomes as empty
RFC 8620 §5.3 lets a server send null instead of an empty map, and
Fastmail does, so every Email/set reply failed to parse.
Assisted-by: Claude Opus 5.5 via Claude Code
diff --git a/src/jmap.rs b/src/jmap.rs
index fd4910d..fd885b7 100644
--- a/src/jmap.rs
+++ b/src/jmap.rs
@@ -860,9 +860,9 @@ struct NewMailbox {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateMailboxesReply {
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
created: BTreeMap<String, Value>,
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
not_created: BTreeMap<String, SetError>,
}
@@ -1010,12 +1010,22 @@ struct EmailPatch {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SetEmailsReply {
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
updated: BTreeMap<String, Option<Value>>,
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
not_updated: BTreeMap<String, SetError>,
}
+/// Reads a `/set` reply's outcome field, which RFC 8620 §5.3 lets the
+/// server send as `null` instead of empty.
+fn null_as_empty<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+ D: de::Deserializer<'de>,
+ T: Deserialize<'de> + Default,
+{
+ Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
+}
+
/// Why the server refused one id in an `Email/set` (RFC 8620 §5.3).
#[derive(Debug, Deserialize)]
struct SetError {
@@ -1054,9 +1064,9 @@ struct DestroyEmails<'a> {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DestroyEmailsReply {
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
destroyed: Vec<String>,
- #[serde(default)]
+ #[serde(default, deserialize_with = "null_as_empty")]
not_destroyed: BTreeMap<String, SetError>,
}
@@ -2559,6 +2569,21 @@ mod tests {
assert!(bare.not_updated.is_empty());
}
+ #[test]
+ fn set_replies_read_null_outcomes_as_empty() {
+ let nulls = json!({
+ "accountId": "a", "oldState": "s1", "newState": "s2",
+ "created": null, "updated": null, "destroyed": null,
+ "notCreated": null, "notUpdated": null, "notDestroyed": null,
+ });
+ let set = parse_set_emails(nulls.clone()).unwrap();
+ assert!(set.updated.is_empty() && set.not_updated.is_empty());
+ let destroy = parse_destroy_emails(nulls.clone()).unwrap();
+ assert!(destroy.destroyed.is_empty() && destroy.not_destroyed.is_empty());
+ let create = parse_create_mailboxes(nulls).unwrap();
+ assert!(create.created.is_empty() && create.not_created.is_empty());
+ }
+
#[test]
fn a_set_error_reads_as_its_type_and_description() {
let error = |v| serde_json::from_value::<SetError>(v).unwrap();