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
impl JmapClient
Sourcepub async fn upload_blob(
&self,
params: UploadBlobParams<'_>,
) -> Result<BlobUploadResponse, ClientError>
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.
Sourcepub async fn download_blob(
&self,
params: DownloadBlobParams<'_>,
) -> Result<Bytes, ClientError>
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.
Sourcepub async fn upload_blob_session(
&self,
session: &Session,
params: UploadBlobSessionParams<'_>,
) -> Result<BlobUploadResponse, ClientError>
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.
Sourcepub async fn download_blob_session(
&self,
session: &Session,
params: DownloadBlobSessionParams<'_>,
) -> Result<Bytes, ClientError>
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
impl JmapClient
Sourcepub fn new(
transport: impl TransportConfig,
auth: impl AuthProvider + 'static,
base_url: &str,
config: ClientConfig,
) -> Result<Self, ClientError>
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.
Sourcepub fn new_plain(
auth: impl AuthProvider + 'static,
base_url: &str,
config: ClientConfig,
) -> Result<Self, ClientError>
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).
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
JmapClientinstances 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
impl JmapClient
Sourcepub async fn fetch_session(&self) -> Result<Session, ClientError>
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).
Sourcepub async fn call(
&self,
api_url: &str,
req: &JmapRequest,
) -> Result<JmapResponse, ClientError>
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).
Sourcepub async fn call_session(
&self,
session: &Session,
req: &JmapRequest,
) -> Result<JmapResponse, ClientError>
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.
Sourcepub async fn subscribe_events(
&self,
event_source_url: &str,
last_event_id: Option<&str>,
) -> Result<BoxStream<'static, Result<SseFrame, ClientError>>, ClientError>
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_buforbufthat have not yet been parsed into anSseFrameare 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_streamheld 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 aslast_event_idon the nextsubscribe_eventscall. 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.
Sourcepub async fn connect_ws_session(
&self,
ws_url: &str,
auth_header: Option<AuthHeader<'_>>,
) -> Result<WsSession, ClientError>
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.
Sourcepub async fn subscribe_events_session(
&self,
session: &Session,
params: SubscribeEventsSessionParams<'_>,
) -> Result<BoxStream<'static, Result<SseFrame, ClientError>>, ClientError>
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
impl Clone for JmapClient
Source§fn clone(&self) -> JmapClient
fn clone(&self) -> JmapClient
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more