Skip to main content

JmapClient

Struct JmapClient 

Source
pub struct JmapClient { /* private fields */ }
Expand description

Auth-agnostic JMAP base HTTP client.

Construct with JmapClient::new or JmapClient::new_plain. Extension-specific clients (jmap-chat-client, jmap-mail-client) depend on this crate and add their method implementations via impl JmapClient.

§Thread-safety (bd:JMAP-6r7c.25)

JmapClient is Send + Sync + Clone. Share by clone across threads or tokio::spawn tasks; the underlying reqwest::Client is reference-counted (Arc-backed) and the AuthProvider trait requires Send + Sync on every implementation. A compile-time assertion in this crate’s test suite pins the Send + Sync contract: a future refactor that adds a non-Sync field (e.g. Rc<T>, RefCell<T>, Cell<T>) would break the assertion in CI before any downstream consumer is exposed.

Implementations§

Source§

impl JmapClient

Source

pub async fn upload_blob( &self, params: UploadBlobParams<'_>, ) -> Result<BlobUploadResponse, ClientError>

Upload raw bytes to the JMAP blob store (RFC 8620 §6.1).

params.upload_url_template is from Session.upload_url; {accountId} is substituted before the request. params.content_type is sent as the Content-Type header. If the server returns a sha256 field (JMAP-CID capability), it is verified against the locally-computed digest and ClientError::BlobIntegrityMismatch is returned on mismatch.

Source

pub async fn download_blob( &self, params: DownloadBlobParams<'_>, ) -> Result<Bytes, ClientError>

Download a blob by ID (RFC 8620 §6.2).

Template variables {accountId}, {blobId}, {name}, and {type} are substituted from the corresponding fields of params before the GET request. {type} expands to an empty string when params.accept_type is None; templates that include ?accept={type} produce ?accept=. If the server does not tolerate an empty ?accept= parameter, omit {type} from the download_url template in the Session document.

If params.expected_sha256 is Some, the downloaded bytes are verified against the typed jmap_cid_types::Sha256 digest and ClientError::BlobIntegrityMismatch is returned on mismatch.

Source

pub async fn upload_blob_session( &self, session: &Session, params: UploadBlobSessionParams<'_>, ) -> Result<BlobUploadResponse, ClientError>

Upload raw bytes via a crate::Session-supplied URL template (bd:JMAP-6r7c.64).

Type-safe convenience wrapper over Self::upload_blob — supplies session.upload_url for upload_url_template internally. The caller cannot accidentally pass session.api_url or any other URL field because the parameter set (UploadBlobSessionParams) does not include a URL field.

Source

pub async fn download_blob_session( &self, session: &Session, params: DownloadBlobSessionParams<'_>, ) -> Result<Bytes, ClientError>

Download a blob via a crate::Session-supplied URL template (bd:JMAP-6r7c.64).

Type-safe convenience wrapper over Self::download_blob — supplies session.download_url for download_url_template internally. See Self::upload_blob_session for the rationale.

Source§

impl JmapClient

Source

pub fn new( transport: impl TransportConfig, auth: impl AuthProvider + 'static, base_url: &str, config: ClientConfig, ) -> Result<Self, ClientError>

Create a new client.

transport configures the underlying HTTP client (TLS trust roots, client certificates, timeouts). auth injects per-request credentials (Bearer token, Basic credentials, or none). The two are independent so any transport can be paired with any credential scheme — for example, CustomCaTransport with BearerAuth. base_url must be the server origin (scheme, host, optional port) with no path, query, or fragment — e.g. "https://100.64.1.1:8008". Trailing slashes are normalized away by the URL parser and are therefore accepted.

Source

pub fn new_plain( auth: impl AuthProvider + 'static, base_url: &str, config: ClientConfig, ) -> Result<Self, ClientError>

Convenience constructor for servers with publicly-trusted TLS.

Equivalent to JmapClient::new(DefaultTransport, auth, base_url, config). Use JmapClient::new when you need a custom transport (e.g. CustomCaTransport for a private-CA server).

Source

pub fn new_with_shared_auth( transport: impl TransportConfig, auth: Arc<dyn AuthProvider>, base_url: &str, config: ClientConfig, ) -> Result<Self, ClientError>

Create a new client sharing an existing Arc<dyn AuthProvider> (bd:JMAP-6r7c.27).

JmapClient::new and new_plain take auth by value and wrap it in a fresh Arc internally. That is the ergonomic case for a caller constructing one client with one auth provider. It is the wrong shape for a caller who:

  • Operates multiple JmapClient instances against different shards or accounts but uses the same credential holder (e.g. a shared OAuth token-refresh state machine, a shared service-account principal).
  • Wants a credential refresh in one client to be visible to all sibling clients without rebuilding each one.

This constructor takes a pre-built Arc<dyn AuthProvider> so callers can clone the Arc and pass clones to multiple JmapClient::new_with_shared_auth calls. The auth provider is then shared by reference-count, and any interior-mutable state (e.g. an RwLock<TokenState> inside a custom OAuthAuth implementation that holds a refreshable bearer) is genuinely shared across all sibling clients.

Arguments mirror JmapClient::new otherwise.

use std::sync::Arc;
use jmap_base_client::{auth::{AuthProvider, BearerAuth, DefaultTransport}, client::{JmapClient, ClientConfig}};

let auth: Arc<dyn AuthProvider> = Arc::new(BearerAuth::new("token")?);
let shard_a = JmapClient::new_with_shared_auth(
    DefaultTransport,
    auth.clone(),
    "https://a.example.com",
    ClientConfig::default(),
)?;
let shard_b = JmapClient::new_with_shared_auth(
    DefaultTransport,
    auth,
    "https://b.example.com",
    ClientConfig::default(),
)?;
Source§

impl JmapClient

Source

pub async fn fetch_session(&self) -> Result<Session, ClientError>

Fetch the JMAP Session object from {base_url}/.well-known/jmap (RFC 8620 §2).

The response body is capped at 1 MiB. Returns ClientError::ResponseTooLarge if the server sends more. Session URL fields (apiUrl, uploadUrl, downloadUrl, eventSourceUrl) are validated to have http/https scheme; a non-http scheme returns ClientError::InvalidSession.

Returns ClientError::AuthFailed on HTTP 401 or 403.

§Charset

The response body MUST be UTF-8-encoded JSON (RFC 8259 §8.1). A server that sends UTF-16 or UTF-32 JSON — even with a matching charset=utf-16 Content-Type parameter — will fail to parse as ClientError::Parse; the error does not specifically call out the charset mismatch. Every shipped JMAP server uses UTF-8, but a non-conformant server can produce a confusing parse error (bd:JMAP-6r7c.28).

Source

pub async fn call( &self, api_url: &str, req: &JmapRequest, ) -> Result<JmapResponse, ClientError>

POST a jmap_types::JmapRequest to api_url and return the parsed jmap_types::JmapResponse (RFC 8620 §3.3).

api_url is taken as an explicit parameter (not from self) because the caller holds a Session and selects the correct URL from it.

The response body is capped at 8 MiB. Returns ClientError::ResponseTooLarge if the server sends more.

Returns ClientError::AuthFailed on HTTP 401 or 403.

§Charset

The response body MUST be UTF-8-encoded JSON (RFC 8259 §8.1). A server that sends UTF-16 or UTF-32 JSON — even with a matching charset=utf-16 Content-Type parameter — will fail to parse as ClientError::Parse; the error does not specifically call out the charset mismatch. Every shipped JMAP server uses UTF-8, but a non-conformant server can produce a confusing parse error (bd:JMAP-6r7c.28).

§See also

Prefer JmapClient::call_session when you have a Session — it picks the correct URL field automatically and prevents the “I passed session.upload_url instead of session.api_url” confusion (bd:JMAP-6r7c.39).

Source

pub async fn call_session( &self, session: &Session, req: &JmapRequest, ) -> Result<JmapResponse, ClientError>

POST a jmap_types::JmapRequest to the api_url field of session and return the parsed response (bd:JMAP-6r7c.39).

Type-safe alternative to JmapClient::call: takes a Session reference and reads session.api_url internally. The “I passed session.upload_url instead of session.api_url” confusion is impossible at the call site because the caller does not select a URL — only Session::api_url is used.

Same body cap, auth, and error semantics as JmapClient::call.

Source

pub async fn subscribe_events( &self, event_source_url: &str, last_event_id: Option<&str>, ) -> Result<BoxStream<'static, Result<SseFrame, ClientError>>, ClientError>

Open an SSE connection to event_source_url and return an async stream of parsed SseFrames (RFC 8620 §7.3).

§URI template expansion

Session.event_source_url is a URI template (RFC 6570 Level-1) with variables types, closeafter, and ping. You must expand it before passing it to this function, or the server will receive the literal text {types} in the URL and return an error. Use expand_url_template:

let url = jmap_base_client::expand_url_template(
    &session.event_source_url,
    &[("types", "*"), ("closeafter", "no"), ("ping", "0")],
)?;
let stream = client.subscribe_events(&url, None).await?;

If last_event_id is Some, sends a Last-Event-ID header so the server can resume from where the previous stream left off.

Buffer growth is capped at ClientConfig::max_sse_frame bytes per frame (default: 1 MiB). If a single SSE frame exceeds this limit the stream yields ClientError::SseFrameTooLarge and terminates.

No timeout is applied to this call or to the resulting stream. The connect timeout (10 s, TCP only) is the only deadline enforced. If the server stalls before sending HTTP response headers, or later goes silent on the open connection, this call or the stream will hang indefinitely. Wrap the entire call and/or stream iteration in tokio::time::timeout if you need to bound either phase.

Returns ClientError::AuthFailed on HTTP 401 or 403 before the stream starts.

§Stream drop and cancellation (bd:JMAP-6r7c.24)

The returned BoxStream may be dropped at any point — mid-frame, while awaiting StreamExt::next, or from inside a tokio::select! losing-branch cancellation. Dropping is always safe and always synchronous:

  • Partial frame bytes are discarded. Any bytes accumulated in the internal raw_buf or buf that have not yet been parsed into an SseFrame are lost. There is no buffering or replay inside the client — the server is the source of truth for what was emitted vs what was acknowledged.
  • The underlying HTTP connection is released. The reqwest::Response::bytes_stream held inside the stream is dropped along with the stream; reqwest returns the connection to its pool (or closes it) per its own pool policy.
  • Resumption is the caller’s job. If you want to resume from the last successfully-parsed frame, capture the most recent SseFrame::id (if the server sets one) and pass it as last_event_id on the next subscribe_events call. The server will replay events from that point per RFC 8895 §9.

tokio::select! cancellation is the canonical use case: a caller racing the SSE stream against a shutdown signal can drop the stream by selecting the shutdown branch without leaking the HTTP connection or memory.

Source

pub async fn connect_ws_session( &self, ws_url: &str, auth_header: Option<AuthHeader<'_>>, ) -> Result<WsSession, ClientError>

Open a WebSocket connection to ws_url using this client’s configured max_ws_message byte cap.

Convenience wrapper around crate::ws::connect_ws_with_limit that passes ClientConfig::max_ws_message as the per-message / per-frame byte cap. Mirrors the Self::subscribe_events pattern of “the JmapClient method uses ClientConfig; the free function takes an explicit value”.

ws_url must come from the session document’s WebSocket capability URL. auth_header is an optional (name, value) pair for the upgrade request; the auth provider on this client is NOT used here because some servers attach WebSocket auth via cookie or session header rather than the same scheme as HTTP requests.

§Security

The auth_header value is a credential and must not be logged or echoed back to other systems. Treat it with the same care as a crate::auth::BearerAuth token. Transport errors raised by this method are constructed without the original credential bytes, but downstream code that inspects ClientError should still avoid printing or storing the auth_header itself.

Returns ClientError::InvalidArgument for non-ws:///wss:// URLs. See crate::ws::connect_ws_with_limit for full error semantics.

Source

pub async fn subscribe_events_session( &self, session: &Session, params: SubscribeEventsSessionParams<'_>, ) -> Result<BoxStream<'static, Result<SseFrame, ClientError>>, ClientError>

Open an SSE connection via a Session-supplied URL template (bd:JMAP-6r7c.64).

Type-safe convenience wrapper over Self::subscribe_events — expands session.event_source_url internally using the caller-supplied SubscribeEventsSessionParams template variables. The caller cannot accidentally pass session.api_url or any other URL field because no URL is exposed at the call site.

Template variables that are None expand to an empty string, per the RFC 8620 §7.3 default-omission semantics. Most JMAP servers accept types= (subscribe to all types), closeafter= (stay-open), and ping= (no server pings) as defaults; if your server requires explicit values, supply them via the params struct.

Trait Implementations§

Source§

impl Clone for JmapClient

Source§

fn clone(&self) -> JmapClient

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for JmapClient

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more