jmap-metadata-types 0.1.0

JMAP Object Metadata extension data types (draft-ietf-jmap-metadata-*)
Documentation

jmap-metadata-types

Serde-annotated Rust types for the JMAP Object Metadata extension (draft-ietf-jmap-metadata-01). Types only — no method handlers, no async, no network I/O.

What it is

Type Description
Metadata Tagged union of Annotation, ImapMetadata, WebDavMetadata (@type discriminator)
Annotation General-purpose vendor metadata (§2.1.1) — round-trips arbitrary domain-prefixed properties via extra
ImapMetadata IMAP METADATA extension mapping (§2.1.2)
WebDavMetadata WebDAV dead-property mapping (§2.1.3)
MetadataCapability Account-level capability object (§1.2.1)
MetadataFilterCondition Filter for Metadata/query (§3.4.1) — filter-algebra; not extras-preserving
MetadataProperty Property selector enum used by /get / /set

Capability URI constant:

Constant Value
JMAP_METADATA_URI "urn:ietf:params:jmap:metadata"

What it's for

draft-ietf-jmap-metadata data types — Metadata, Annotation, ImapMetadata, WebDavMetadata, MetadataFilterCondition, and MetadataCapability — consumed by jmap-metadata-server (method handlers + MetadataBackend trait) and jmap-metadata-client (client-side method bindings). This crate is the workspace-recommended IETF-track escape hatch for vendor data that needs to be queryable: the workspace extras-preservation policy excludes filter algebra (silent-drop is a query-correctness bug), so vendors who need filterable metadata route through this object rather than via a vendor-extended filter condition.

Why this crate exists

jmap-types and the extension-types crates apply an extras-preservation policy that captures vendor fields in a flatten extra map on every wire-format Deserialize struct. That policy explicitly excludes filter and comparator algebra: silently round-tripping an unknown filter clause through the client gives the wrong record set back with no error signal.

draft-ietf-jmap-metadata-01 is the IETF-track answer. It defines a companion Metadata object keyed by (relatedType, relatedId) with schema discovery via the capability and a Metadata/query filter that includes textMatch over vendor string properties — coarse but standardised filterability that the base extras policy cannot offer.

This crate is the foundation of the workspace's implementation tracker, bd JMAP-06zp.

Spec coverage

draft-ietf-jmap-metadata-01 sections implemented:

  • §1.2.1 — MetadataCapability and JMAP_METADATA_URI
  • §2.1 — Metadata tagged union; three variants
  • §2.1.1 / §2.2.1.6 — Annotation with vendor-extension support
  • §2.1.2 / §2.2.2 — ImapMetadata with /private/ and /shared/ namespace mapping
  • §2.1.3 / §2.2.3 — WebDavMetadata with expanded-name property keys
  • §2.2.1 — Common properties (id, @type, relatedType, relatedId, isPrivate)
  • §3.4.1 — MetadataFilterCondition
  • IANA §9.1 — capability URI string

Not in this crate (tracked separately under bd JMAP-06zp):

  • Method handlers Metadata/get, Metadata/set, Metadata/changes, Metadata/query, Metadata/queryChanges (live in jmap-metadata-server, bd JMAP-06zp.3).
  • The standard-method extensions (fetchMetadata, onSuccessCreateMetadata, onSuccessUpdateMetadata, metadata: Metadata[] response field) live in each consumer crate's /get and /set argument structs (bd JMAP-06zp.5).

Filter-algebra exclusion

MetadataFilterCondition does NOT carry a extra flatten field — the workspace extras-preservation policy explicitly excludes filter algebra (see workspace AGENTS.md and bd JMAP-lbdy). Vendor extras that need to be filterable belong in an Annotation payload and are queried via the spec's own textMatch filter, not via a vendor-extended filter condition.

How to use

use jmap_metadata_types::{Annotation, Metadata};

// Deserialize an Annotation (the §7.5 atomic-create response shape).
let meta: Metadata = serde_json::from_str(r#"{
    "@type": "Annotation",
    "id": "MD789",
    "relatedType": "Email",
    "relatedId": "EM456",
    "isPrivate": true,
    "acme.example.com:workflowState": "pending-review"
}"#).unwrap();

match meta {
    Metadata::Annotation(Annotation { related_type, ref extra, .. }) => {
        assert_eq!(related_type, "Email");
        assert_eq!(
            extra.get("acme.example.com:workflowState").and_then(|v| v.as_str()),
            Some("pending-review"),
        );
    }
    _ => unreachable!(),
}

How it works

The Metadata enum is #[serde(tag = "@type")] — the wire @type discriminator is consumed by serde automatically. Each variant struct carries the common properties (id, relatedType, relatedId, isPrivate) as typed fields plus an extra flatten map for vendor / future-spec fields.

All structs carry #[serde(rename_all = "camelCase")] to produce camelCase JSON field names as required by the JMAP wire format. MetadataFilterCondition.type_names is named for clarity in Rust but serialises as the literal wire name "@type" via #[serde(rename = "@type")].

ImapMetadata and WebDavMetadata use BTreeMap<String, String> for their metadata field so round-trip output is deterministic.

Gotchas

Tests are named with _draft_01_ to pin them to the current draft-ietf-jmap-metadata revision. When the draft revs, expect to update or replace these tests alongside the wire-format changes. The #[non_exhaustive] derive on every struct and enum keeps additive spec evolution non-breaking, but a renamed or removed wire field would still be a breaking change in the next 0.x bump.

MetadataFilterCondition does not carry the workspace extras-preservation extra field — see the Filter-algebra exclusion section above and the workspace AGENTS.md for the rationale.

References