jmap-contacts-client 0.1.1

JMAP Contacts HTTP client — extension trait over jmap-base-client
Documentation

jmap-contacts-client

What it is

Typed client methods for JMAP Contacts (RFC 9610). Depends on jmap-base-client for transport, authentication, and session management.

What it's for

Implements draft-ietf-jmap-contacts (RFC 9610) method bindings on top of jmap-base-client: AddressBook/get|changes|set and ContactCard/get|changes|set|copy|query|queryChanges. Sibling of jmap-mail-client in the extension-client family — mirrors that crate's shape. Depends on jmap-base-client for transport and session, and on jmap-contacts-types for the wire types (including the RFC 9553 JSContact sub-types re-exported under the jscontact module alias).

How to use

use jmap_base_client::{BearerAuth, ClientConfig, JmapClient};
use jmap_contacts_client::{JmapContactsExt, AddressBookSetParams};
use jmap_contacts_types::ContactCard;
use jmap_types::{Id, State};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Build a base client (handles auth, HTTP, session fetch).
    let auth = BearerAuth::new("my-token")?;
    let client = JmapClient::new_plain(auth, "https://jmap.example.com", ClientConfig::default())?;

    // 2. Fetch the JMAP session document.
    let session = client.fetch_session().await?;

    // 3. Bind the session to a contacts client.
    let contacts = client.with_contacts_session(session);

    // 4. Fetch all address books.
    let response = contacts.address_book_get(None, None).await?;
    for ab in &response.list {
        println!("{}: {}", ab.id, ab.name);
    }

    // 5. Fetch all contact cards.
    let cards = contacts.contact_card_get(None, None).await?;
    for card in &cards.list {
        if let Some(id) = &card.id {
            println!("card id={}", id);
        }
    }

    // 6. Create a new ContactCard.
    //    JSContact sub-objects (name, emails, etc.) must be built as
    //    serde_json::Value — no typed builder is provided (see Known Limitations).
    let create_card = json!({
        "newCard": {
            "addressBookIds": { "ab-id-here": true },
            "version": "1.0",
            "uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
            "name": {
                "components": [
                    { "kind": "given", "value": "Jane" },
                    { "kind": "surname", "value": "Smith" }
                ],
                "isOrdered": true
            },
            "emails": {
                "e1": {
                    "contexts": { "work": true },
                    "address": "jane@example.com"
                }
            }
        }
    });
    let set_resp = contacts
        .contact_card_set(Some(create_card), None, None)
        .await?;
    if let Some(created) = set_resp.created {
        if let Some(card) = created.get("newCard") {
            println!("created card id={:?}", card.id);
        }
    }

    // 7. Query contact cards in a specific address book.
    let filter = json!({ "inAddressBook": "ab-id-here" });
    let query = contacts
        .contact_card_query(Some(filter), None, None, None)
        .await?;
    println!("found {} card(s)", query.ids.len());

    Ok(())
}

Id parameters are typed &jmap_types::Id (or &[jmap_types::Id] for slices) to make invalid Ids unrepresentable. State tokens use &jmap_types::State. Construct Ids with Id::new_validated(s) to enforce RFC 8620 §1.2 syntax at the boundary, or with Id::from(s) when the value is known-valid (e.g. already came back from a server response).

After constructing a SessionClient via with_contacts_session, all JMAP Contacts methods are available without passing &Session on every call. If the session expires, re-fetch with JmapClient::fetch_session and construct a new SessionClient.

Registered methods

All method implementations live on SessionClient in the methods/ submodules.

Method Function Returns
AddressBook/get address_book_get GetResponse<AddressBook>
AddressBook/changes address_book_changes ChangesResponse
AddressBook/set address_book_set SetResponse<AddressBook>
ContactCard/get contact_card_get GetResponse<ContactCard>
ContactCard/changes contact_card_changes ChangesResponse
ContactCard/set contact_card_set SetResponse<ContactCard>
ContactCard/copy contact_card_copy SetResponse<ContactCard>
ContactCard/query contact_card_query QueryResponse
ContactCard/queryChanges contact_card_query_changes QueryChangesResponse

Note: AddressBook/query and AddressBook/queryChanges are not implemented — RFC 9610 does not define these methods.

AddressBook/set — extra arguments

address_book_set accepts an optional AddressBookSetParams for the contacts-specific method-level arguments:

pub struct AddressBookSetParams {
    /// When true, ContactCards belonging only to a destroyed AddressBook
    /// are also destroyed; cards in multiple books are detached.
    pub on_destroy_remove_contents: Option<bool>,

    /// Designates one AddressBook as the account default after all other
    /// operations succeed (RFC 9610 §2.3).
    pub on_success_set_is_default: Option<serde_json::Value>,
}

Pass None for params (or Some(Default::default())) when neither argument is needed.

How it works

Every method follows the same five-step pattern:

  1. Validate arguments (defence-in-depth empty-state guards return InvalidArgument before any network call; Id-shaped parameters are typed &Id / &[Id] so empty-Id inputs are unrepresentable).
  2. Call session_parts() to extract (api_url, account_id) from the bound session.
  3. Build the JMAP method arguments as a serde_json::Value.
  4. Call build_request(method_name, args, USING_CONTACTS) to construct a single-method JmapRequest.
  5. POST to the API URL and extract the typed response via jmap_base_client::extract_response.

The capability using array for all contacts requests is: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:contacts"].

Gotchas

  • Creating or updating a ContactCard requires the caller to build serde_json::Value for all JSContact sub-objects (name, phones, addresses, emails, etc.) manually. No typed builder or fluent API is provided. Use RFC 9553 as the schema when constructing these values.
  • contact_card_query and contact_card_query_changes accept filter and sort as untyped serde_json::Value. Use ContactCardFilterCondition and ContactCardComparator from jmap-contacts-types and serialize them with serde_json::to_value before passing.
  • No integration tests against a real JMAP server. Tests use direct serialization assertions against known JSON shapes from the spec.

Crate family

jmap-types
    └── jmap-base-client         transport, auth, session
            └── jmap-contacts-client  ← this crate
                    (also depends on jmap-contacts-types for response types)

References

  • RFC 9610 — JMAP Contacts (normative for all method names, argument shapes, and response formats)
  • RFC 9553 — JSContact (normative for ContactCard sub-object schemas)
  • RFC 8620 — JMAP Core (request format, response shapes, /get, /set, /changes, /query, /queryChanges, /copy)