jmap-mail-client
What it is
RFC 8621 JMAP for Mail client methods — typed bindings on top of jmap-base-client.
Typed JMAP client methods for JMAP Mail (RFC 8621). An extension trait on
jmap-base-client::JmapClient that adds all 26 RFC 8621 method calls as typed
async methods.
What it's for
Implements RFC 8621 method bindings (Email/get, Mailbox/get/set/changes,
Thread/get, EmailSubmission/set, Identity/get, SearchSnippet/get,
MDN/send, etc.) on top of jmap-base-client. This crate is the canonical
template for every extension *-client crate in the workspace — chat, calendars,
tasks, contacts, filenode, sharing, and metadata all mirror its module layout
and method-shape conventions. Depends on jmap-base-client for transport and
session, and on jmap-mail-types for the wire types.
How to use
use ;
use ;
use Id;
async
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 calling JmapMailExt::with_mail_session(session) the returned
[SessionClient] carries the session and makes it available to all methods
without requiring the caller to pass it again. Construct a new SessionClient
after each fetch_session call — do not reuse a stale one across session
state changes.
Registered methods
All 26 RFC 8621 method names are available as typed async methods on
[SessionClient]:
| Method | Parameters | Returns |
|---|---|---|
email_get |
ids: Option<&[Id]>, properties: Option<&[&str]>, params: Option<EmailGetParams> |
GetResponse<Email> |
email_changes |
since_state: &State, max_changes: Option<u64> |
ChangesResponse |
email_set |
create: Option<Value>, update: Option<HashMap<Id, PatchObject>>, destroy: Option<Vec<Id>>, if_in_state: Option<&State> |
SetResponse<Email> |
email_query |
filter: Option<Value>, sort: Option<Value>, position: Option<u64>, limit: Option<u64>, collapse_threads: Option<bool> |
QueryResponse |
email_query_changes |
since_query_state: &State, max_changes: Option<u64>, collapse_threads: Option<bool>, filter: Option<Value>, sort: Option<Value>, up_to_id: Option<&Id>, calculate_total: Option<bool> |
QueryChangesResponse |
email_copy |
params: EmailCopyParams, create: Value |
SetResponse<Email> |
email_import |
emails: &HashMap<String, EmailImportInput>, if_in_state: Option<&State> |
EmailImportResponse |
email_parse |
blob_ids: &[Id], params: Option<EmailParseParams> |
EmailParseResponse |
mailbox_get |
ids: Option<&[Id]>, properties: Option<&[&str]> |
GetResponse<Mailbox> |
mailbox_changes |
since_state: &State, max_changes: Option<u64> |
ChangesResponse |
mailbox_set |
create: Option<Value>, update: Option<HashMap<Id, PatchObject>>, destroy: Option<Vec<Id>>, params: Option<MailboxSetParams> |
SetResponse<Mailbox> |
mailbox_query |
filter: Option<Value>, sort: Option<Value>, position: Option<u64>, limit: Option<u64> |
QueryResponse |
mailbox_query_changes |
since_query_state: &State, max_changes: Option<u64>, filter: Option<Value>, sort: Option<Value>, up_to_id: Option<&Id>, calculate_total: Option<bool> |
QueryChangesResponse |
thread_get |
ids: Option<&[Id]>, properties: Option<&[&str]> |
GetResponse<Thread> |
thread_changes |
since_state: &State, max_changes: Option<u64> |
ChangesResponse |
identity_get |
ids: Option<&[Id]>, properties: Option<&[&str]> |
GetResponse<Identity> |
identity_changes |
since_state: &State, max_changes: Option<u64> |
ChangesResponse |
identity_set |
create: Option<Value>, update: Option<HashMap<Id, PatchObject>>, destroy: Option<Vec<Id>> |
SetResponse<Identity> |
search_snippet_get |
filter: Value, email_ids: Option<&[Id]> |
Value |
email_submission_get |
ids: Option<&[Id]>, properties: Option<&[&str]> |
GetResponse<EmailSubmission> |
email_submission_changes |
since_state: &State, max_changes: Option<u64> |
ChangesResponse |
email_submission_query |
filter: Option<Value>, sort: Option<Value>, position: Option<u64>, limit: Option<u64> |
QueryResponse |
email_submission_query_changes |
since_query_state: &State, max_changes: Option<u64>, filter: Option<Value>, sort: Option<Value>, up_to_id: Option<&Id>, calculate_total: Option<bool> |
QueryChangesResponse |
email_submission_set |
create: Option<Value>, update: Option<HashMap<Id, PatchObject>>, destroy: Option<Vec<Id>>, if_in_state: Option<&State>, params: Option<EmailSubmissionSetParams> |
SetResponse<EmailSubmission> |
vacation_response_get |
(none) | VacationResponse (unwraps the spec-mandated singleton from the /get envelope) |
vacation_response_get_envelope |
(none) | GetResponse<VacationResponse> (envelope including state token for ifInState guards) |
vacation_response_set |
update: Option<HashMap<Id, PatchObject>> |
SetResponse<VacationResponse> |
Id and State here are jmap_types::Id and jmap_types::State.
EmailSubmissionSetParams
EmailSubmissionSetParams carries two method-level fields that are sent at the
top level of the EmailSubmission/set request body, not inside a create or
update object:
Both fields are omitted from the JSON payload when None.
EmailGetParams
EmailGetParams controls which body content the server includes in an
Email/get response (RFC 8621 §4.1.8). All fields default to None (server
default):
Fields set to None are omitted from the request; the server uses its own
defaults for omitted fields.
Response types
| Type | RFC section | Description |
|---|---|---|
GetResponse<T> |
RFC 8620 §5.1 | /get response: account_id, state, list, not_found |
ChangesResponse |
RFC 8620 §5.2 | /changes response: old_state, new_state, has_more_changes, created, updated, destroyed |
SetResponse<T> |
RFC 8620 §5.3 | /set response: created, updated, destroyed, not_created, not_updated, not_destroyed |
QueryResponse |
RFC 8620 §5.5 | /query response: query_state, can_calculate_changes, position, ids, total, limit |
QueryChangesResponse |
RFC 8620 §5.6 | /queryChanges response: old_query_state, new_query_state, removed, added |
AddedItem |
RFC 8620 §5.6 | Entry in QueryChangesResponse::added: id and index |
SetResponse<T> defaults to SetResponse<serde_json::Value> when no type
parameter is given. Use SetResponse<Email> to get typed created/updated maps.
How it works
Every method on SessionClient follows the same six-step pipeline:
- Validate arguments — defence-in-depth empty-state guards fire before any
I/O, returning
ClientError::InvalidArgumentimmediately. Id-shaped parameters are typed&Id/&[Id]and validated by construction (Id::new_validated); the production code does not re-validate Id syntax. session_parts()— extracts(api_url, account_id)from the bound session; returnsClientError::InvalidSessionif there is no primary account forurn:ietf:params:jmap:mail.- Build args JSON — constructs the
serde_json::Valueargument object, merging in any extra params structs by iterating their key-value pairs. build_request(method, args, USING_*)— wraps the single invocation into aJmapRequestwith the appropriateusingarray per RFC 8621 §1.3:USING_MAILfor Email/Mailbox/Thread/SearchSnippet methods,USING_SUBMISSIONfor Identity/EmailSubmission methods (includes bothurn:ietf:params:jmap:mailandurn:ietf:params:jmap:submission), andUSING_VACATIONfor VacationResponse methods. Call ID is"r1".call_internal(api_url, &req)— delegates tojmap_base_client::JmapClient::call, which POSTs the request and returns aJmapResponse.extract_response(&resp, CALL_ID)— finds the invocation for call ID"r1"in the response and deserializes it into the typed return value.
Examples
The crate ships one runnable example under examples/ demonstrating the
end-to-end client flow:
examples/mailbox_list.rs— fetch and print all mailboxes from a synthetic JMAP server (wiremock mock with a hand-writtenMailbox/getresponse covering the common RFC 8621 §2.1 roles). Run:cargo run --example mailbox_list -p jmap-mail-client.
NOT FOR PRODUCTION — synthetic mock-server fixtures only, no auth, no TLS. Demonstrates the consume-side API.
Gotchas
email_importrequires a separately uploaded blob. RFC 8621 §4.8Email/importoperates on a previously uploaded raw RFC 5322 message; callers must upload the raw bytes viajmap_base_client::JmapClient's blob-upload API and pass the resultingblob_idtoEmailImportInput. Likewise,email_parseoperates on blobs already in the store.- Partial
Email/getviapropertiesfiltering breaks deserialization.Emailhas six required metadata fields (id,blob_id,thread_id,mailbox_ids,keywords,size,received_at). If the server omits any of these because of apropertiesfilter,GetResponse<Email>will fail to deserialize. UseGetResponse<serde_json::Value>for partial-field responses. - No streaming API for large email bodies. All body values returned by
Email/getwithfetch_*_body_valuesoptions are buffered in memory as part of the JMAP response. Very large messages should be downloaded viaJmapClient::download_blobinstead. - Implicit
Email/setfromEmailSubmission/set. WhenEmailSubmissionSetParams::on_success_update_emailoron_success_destroy_emailis set, the server generates an implicitEmail/setinvocation and includes it in the response.extract_responseextracts only theEmailSubmission/setresult identified by call ID"r1". Callers that need to inspect the implicitEmail/setresult must calljmap_base_client::JmapClient::calldirectly and iterateJmapResponse::method_responsesthemselves.
Crate family
jmap-types
├── jmap-mail-types Email, Mailbox, Thread, Identity, etc.
│ └── jmap-mail-client ← this crate
└── jmap-base-client transport, session, auth
└── (also a dep of jmap-mail-client)
Path dependencies between crates use path = "../crate-jmap-*" and will
remain that way until the family is published to crates.io.