jmap-mail-client 0.1.2

RFC 8621 JMAP for Mail client — Email, Mailbox, Thread, Identity, EmailSubmission methods
Documentation
# jmap-mail-client — Implementation Plan

RFC 8621 (JMAP for Mail) method implementations on top of `jmap-base-client`.

## Crate Family Position

```
jmap-types
    ├── jmap-mail-types
    │       └── (types used here)
    └── jmap-base-client
            └── jmap-mail-client  ← this crate
```

## What This Crate Is

An extension layer over `jmap-base-client` that adds typed methods for every RFC 8621
operation: `Email/get`, `Email/set`, `Email/query`, `Email/changes`, `Email/copy`,
`Mailbox/get`, `Mailbox/set`, `Mailbox/query`, `Mailbox/changes`, `Thread/get`,
`Thread/changes`, `Identity/get`, `Identity/set`, `Identity/changes`,
`EmailSubmission/get`, `EmailSubmission/changes`, `EmailSubmission/query`,
`EmailSubmission/queryChanges`, `EmailSubmission/set`,
`SearchSnippet/get`, `VacationResponse/get`, `VacationResponse/set`.

Consumers call `jmap-base-client::JmapClient::call()` directly or use the typed helpers
defined here. No new HTTP machinery — all network operations go through `jmap-base-client`.

Known potential consumers: a future CLI mail client, or `stoa` if it ever grows a
client-side sync path.

## What This Crate Is Not

- Not a server-side crate
- Not a standalone HTTP client (no auth, no transport — that's `jmap-base-client`)
- Not handling IMAP, SMTP, or other non-JMAP mail protocols

## Source Material

This is **greenfield** — no existing Rust implementation to extract from.

Design pattern to follow:
- `~/PROJECT/crate-jmapchat-client/src/methods/` — how method inputs/outputs are
  structured and how `JmapRequestBuilder` is used to issue calls
- `~/PROJECT/jmap-chat-spec/references/rfc8621.txt` — normative spec

## Dependencies

```toml
jmap-types      = { path = "../crate-jmap-types" }
jmap-mail-types = { path = "../crate-jmap-mail-types" }
jmap-base-client     = { path = "../crate-jmap-base-client" }
serde_json      = "1"
thiserror       = "2"
```

No direct reqwest/tokio dependency — all I/O goes through `jmap-base-client`.

## Extension Trait Pattern

Cross-crate inherent impls are not valid Rust (orphan rule: only the crate that defines
a type may add inherent methods to it). To add methods to `JmapClient` from this crate,
we use an **extension trait**:

```rust
pub trait JmapMailExt {
    async fn email_get(...) -> Result<...>;
}

impl JmapMailExt for JmapClient {
    async fn email_get(...) -> Result<...> { ... }
}
```

Callers must bring the trait into scope: `use jmap_mail_client::JmapMailExt;`

Rust 1.75 AFIT (async fn in trait, via RPITIT) is used — no `async-trait` crate needed.
This works because we do not need `dyn JmapMailExt`. If dyn dispatch is ever required,
wrap with `async-trait 0.1` at that time.

## Planned Public API

The shipped API uses the `SessionClient` shape (extension trait returns a
session-bound client; methods are inherent on `SessionClient`). Id-shaped
parameters are `&Id` / `&[Id]` / `Option<&[Id]>` and state-shaped parameters
are `&State` since bd:JMAP-6by7.2 (2026-05-09); the brief drift to
`&str` / `&[&str]` from earlier 0.1.x preview revisions was reverted in
that bead. The sketch below uses the trait-method shape from the original
plan; the actual shipped API on `SessionClient` is documented in the
README "Registered methods" table.

```rust
use jmap_base_client::{ClientError, JmapClient};
use jmap_mail_types::{Email, Mailbox, Thread, Identity, EmailSubmission, SearchSnippet};
use jmap_types::{Id, State};

/// Extension trait adding RFC 8621 (JMAP for Mail) methods to [`JmapClient`].
///
/// Import this trait to use: `use jmap_mail_client::JmapMailExt;`
pub trait JmapMailExt {
    // Email
    async fn email_get(&self, account_id: &Id, ids: &[Id], props: &[&str])
        -> Result<GetResponse<Email>, ClientError>;
    async fn email_set(&self, account_id: &Id, req: SetRequest<Email>)
        -> Result<SetResponse<Email>, ClientError>;
    async fn email_query(&self, account_id: &Id, req: EmailQueryRequest)
        -> Result<QueryResponse, ClientError>;
    async fn email_changes(&self, account_id: &Id, since_state: &State, max: Option<u64>)
        -> Result<ChangesResponse, ClientError>;

    // Mailbox
    async fn mailbox_get(&self, account_id: &Id, ids: Option<&[Id]>)
        -> Result<GetResponse<Mailbox>, ClientError>;
    async fn mailbox_set(&self, account_id: &Id, req: SetRequest<Mailbox>)
        -> Result<SetResponse<Mailbox>, ClientError>;

    // Thread
    async fn thread_get(&self, account_id: &Id, ids: &[Id])
        -> Result<GetResponse<Thread>, ClientError>;

    // Identity
    async fn identity_get(&self, account_id: &Id, ids: Option<&[Id]>)
        -> Result<GetResponse<Identity>, ClientError>;

    // EmailSubmission
    async fn email_submission_set(&self, account_id: &Id, req: SetRequest<EmailSubmission>)
        -> Result<SetResponse<EmailSubmission>, ClientError>;

    // SearchSnippet
    async fn search_snippet_get(&self, account_id: &Id, filter: serde_json::Value, email_ids: &[Id])
        -> Result<Vec<SearchSnippet>, ClientError>;
}

impl JmapMailExt for JmapClient {
    // implementations in email.rs, mailbox.rs, thread.rs, identity.rs, submission.rs, snippet.rs
}
```

## Module Layout

```
src/
  lib.rs        pub trait JmapMailExt; impl JmapMailExt for JmapClient; re-exports
  email.rs      Email/get, Email/set, Email/query, Email/changes request/response types
  mailbox.rs    Mailbox/get, Mailbox/set, Mailbox/query request/response types
  thread.rs     Thread/get, Thread/changes request/response types
  identity.rs   Identity/get request/response types
  submission.rs EmailSubmission/set request/response types
  snippet.rs    SearchSnippet/get request/response types
```

## Extras-preservation policy (JMAP-lbdy)

Every public method-argument and method-response struct defined in this
crate that appears on the JMAP wire carries an `extra` field per the
workspace extras-preservation policy (see workspace `AGENTS.md`):

```rust
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
```

This preserves vendor / site / private-extension fields across
deserialize/serialize round-trip. Wire format is byte-identical when extras
are empty. The `default` attribute is inert for Serialize-only method-arg
structs and active for Deserialize method-response structs; the attribute
set is kept uniform across both directions of wire flow.

In scope in this crate (each has at least one round-trip preservation
test in `methods/mod.rs`):

- Method-argument structs (Serialize-only): `EmailGetParams`,
  `EmailCopyParams`, `MailboxSetParams`, `EmailSubmissionSetParams`,
  `EmailImportInput`, `EmailParseParams`.
- Method-response structs (Deserialize): `EmailImportCreated`,
  `EmailImportResponse`, `EmailParseResponse`.

The crate also re-exports standard response wrappers (`GetResponse<T>`,
`SetResponse<T>`, `ChangesResponse`, `QueryResponse`,
`QueryChangesResponse`) from `jmap-types`; those carry their own `extra`
field per JMAP-lbdy.1 and are not re-documented here.

Out of scope (explicitly excluded by the workspace policy):

- Filter / comparator algebra types and control enums — see workspace
  AGENTS.md "Filter algebra and control enums are explicitly EXCLUDED"
  for the full rationale.
- Internal Rust state types (`SessionClient`) — not wire-format.

### New-type rule

Any new public method-argument or method-response struct added to this
crate that appears on the JMAP wire MUST include the `extra` field from
day one with the documented serde attributes and at least one round-trip
preservation test. Per the canonical-template propagation rule (workspace
AGENTS.md), changes to this crate's surface propagate to the six sibling
extension-client crates in lock-step.

## Test Strategy

- All tests use `wiremock` via `jmap-base-client`'s HTTP layer — no live network
- Request serialization tests: construct a typed request, verify JSON matches RFC 8621 examples
- Response deserialization tests: feed RFC 8621 example JSON, verify typed structs
- RFC 8621 §1.5 example exchange used as the primary oracle