Skip to main content

io_jmap/
client.rs

1//! Standard, blocking JMAP client.
2//!
3//! Wraps a single boxed stream plus the bearer token and discovered
4//! [`JmapSession`], with one method per common coroutine. [`JmapClientStd::new`]
5//! takes a pre-connected stream; with one of the TLS features enabled,
6//! [`JmapClientStd::connect`] handles `https://` URLs end-to-end.
7//!
8//! Run [`JmapClientStd::session_get`] once after construction to populate the
9//! session; subsequent calls resolve `accountId` and `apiUrl` from it.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use io_jmap::client::JmapClientStd;
15//! use pimalaya_stream::tls::Tls;
16//! use secrecy::SecretString;
17//! use url::Url;
18//!
19//! let url: Url = "https://api.example.com/jmap/session/".parse().unwrap();
20//! let auth = SecretString::from("Bearer xyz");
21//!
22//! let mut client = JmapClientStd::connect(&url, &Tls::default(), auth).unwrap();
23//! let session = client.session_get(&url).unwrap();
24//!
25//! println!("logged in as {}", session.username);
26//! ```
27
28#[cfg(any(
29    feature = "rustls-aws",
30    feature = "rustls-ring",
31    feature = "native-tls"
32))]
33use core::time::Duration;
34use core::{any::Any, fmt};
35
36#[cfg(any(
37    feature = "rustls-aws",
38    feature = "rustls-ring",
39    feature = "native-tls"
40))]
41use alloc::string::ToString;
42use alloc::{boxed::Box, collections::BTreeMap, string::String, vec, vec::Vec};
43
44use std::io::{self, Read, Write};
45
46#[cfg(any(
47    feature = "rustls-aws",
48    feature = "rustls-ring",
49    feature = "native-tls"
50))]
51use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
52use secrecy::SecretString;
53use thiserror::Error;
54use url::Url;
55
56use crate::{
57    coroutine::*,
58    rfc8620::{
59        blob_download::*,
60        blob_upload::*,
61        changes::JmapChangesOutput,
62        coroutine::JmapRedirectYield,
63        push_subscription::{get::*, set::*},
64        request::{JmapRequest, JmapResponse},
65        send::*,
66        session::JmapSession,
67        session_get::*,
68    },
69    rfc8621::{
70        email::{changes::*, copy::*, get::*, import::*, parse::*, query::*, set::*},
71        email_submission::{cancel::*, get::*, query::*, set::*},
72        identity::{get::*, set::*},
73        mailbox::{changes::*, get::*, query::*, set::*},
74        thread::{changes::*, get::*},
75        vacation_response::{get::*, set::*, *},
76    },
77    rfc9610::{
78        address_book::{changes::*, get::*, set::*},
79        contact_card::{changes::*, copy::*, get::*, query::*, set::*},
80    },
81};
82
83/// Errors returned by [`JmapClientStd`].
84#[derive(Debug, Error)]
85pub enum JmapClientStdError {
86    /// The raw send coroutine failed.
87    #[error(transparent)]
88    Send(#[from] JmapSendError),
89    /// The session fetch coroutine failed.
90    #[error(transparent)]
91    SessionGet(#[from] JmapSessionGetError),
92    /// The blob upload coroutine failed.
93    #[error(transparent)]
94    BlobUpload(#[from] JmapBlobUploadError),
95    /// The blob download coroutine failed.
96    #[error(transparent)]
97    BlobDownload(#[from] JmapBlobDownloadError),
98    /// The `PushSubscription/get` coroutine failed.
99    #[error(transparent)]
100    PushSubscriptionGet(#[from] JmapPushSubscriptionGetError),
101    /// The `PushSubscription/set` coroutine failed.
102    #[error(transparent)]
103    PushSubscriptionSet(#[from] JmapPushSubscriptionSetError),
104    /// The `Mailbox/get` coroutine failed.
105    #[error(transparent)]
106    MailboxGet(#[from] JmapMailboxGetError),
107    /// The `Mailbox/query` coroutine failed.
108    #[error(transparent)]
109    MailboxQuery(#[from] JmapMailboxQueryError),
110    /// The `Mailbox/set` coroutine failed.
111    #[error(transparent)]
112    MailboxSet(#[from] JmapMailboxSetError),
113    /// The `Mailbox/changes` coroutine failed.
114    #[error(transparent)]
115    MailboxChanges(#[from] JmapMailboxChangesError),
116    /// The `Email/get` coroutine failed.
117    #[error(transparent)]
118    EmailGet(#[from] JmapEmailGetError),
119    /// The `Email/query` coroutine failed.
120    #[error(transparent)]
121    EmailQuery(#[from] JmapEmailQueryError),
122    /// The `Email/set` coroutine failed.
123    #[error(transparent)]
124    EmailSet(#[from] JmapEmailSetError),
125    /// The `Email/changes` coroutine failed.
126    #[error(transparent)]
127    EmailChanges(#[from] JmapEmailChangesError),
128    /// The `Email/copy` coroutine failed.
129    #[error(transparent)]
130    EmailCopy(#[from] JmapEmailCopyError),
131    /// The `Email/import` coroutine failed.
132    #[error(transparent)]
133    EmailImport(#[from] JmapEmailImportError),
134    /// The `Email/parse` coroutine failed.
135    #[error(transparent)]
136    EmailParse(#[from] JmapEmailParseError),
137    /// The `Thread/get` coroutine failed.
138    #[error(transparent)]
139    ThreadGet(#[from] JmapThreadGetError),
140    /// The `Thread/changes` coroutine failed.
141    #[error(transparent)]
142    ThreadChanges(#[from] JmapThreadChangesError),
143    /// The `Identity/get` coroutine failed.
144    #[error(transparent)]
145    IdentityGet(#[from] JmapIdentityGetError),
146    /// The `Identity/set` coroutine failed.
147    #[error(transparent)]
148    IdentitySet(#[from] JmapIdentitySetError),
149    /// The `EmailSubmission/get` coroutine failed.
150    #[error(transparent)]
151    EmailSubmissionGet(#[from] JmapEmailSubmissionGetError),
152    /// The `EmailSubmission/query` coroutine failed.
153    #[error(transparent)]
154    EmailSubmissionQuery(#[from] JmapEmailSubmissionQueryError),
155    /// The `EmailSubmission/set` coroutine failed.
156    #[error(transparent)]
157    EmailSubmissionSet(#[from] JmapEmailSubmissionSetError),
158    /// The `EmailSubmission/set` cancel coroutine failed.
159    #[error(transparent)]
160    EmailSubmissionCancel(#[from] JmapEmailSubmissionCancelError),
161    /// The `VacationResponse/get` coroutine failed.
162    #[error(transparent)]
163    VacationResponseGet(#[from] JmapVacationResponseGetError),
164    /// The `VacationResponse/set` coroutine failed.
165    #[error(transparent)]
166    VacationResponseSet(#[from] JmapVacationResponseSetError),
167    /// The `AddressBook/get` coroutine failed.
168    #[error(transparent)]
169    AddressBookGet(#[from] JmapAddressBookGetError),
170    /// The `AddressBook/set` coroutine failed.
171    #[error(transparent)]
172    AddressBookSet(#[from] JmapAddressBookSetError),
173    /// The `AddressBook/changes` coroutine failed.
174    #[error(transparent)]
175    AddressBookChanges(#[from] JmapAddressBookChangesError),
176    /// The `ContactCard/get` coroutine failed.
177    #[error(transparent)]
178    ContactCardGet(#[from] JmapContactCardGetError),
179    /// The `ContactCard/query` coroutine failed.
180    #[error(transparent)]
181    ContactCardQuery(#[from] JmapContactCardQueryError),
182    /// The `ContactCard/set` coroutine failed.
183    #[error(transparent)]
184    ContactCardSet(#[from] JmapContactCardSetError),
185    /// The `ContactCard/changes` coroutine failed.
186    #[error(transparent)]
187    ContactCardChanges(#[from] JmapContactCardChangesError),
188    /// The `ContactCard/copy` coroutine failed.
189    #[error(transparent)]
190    ContactCardCopy(#[from] JmapContactCardCopyError),
191    /// The underlying stream failed to read or write.
192    #[error(transparent)]
193    Io(#[from] io::Error),
194    /// The TCP connection or the TLS negotiation failed.
195    #[cfg(any(
196        feature = "rustls-aws",
197        feature = "rustls-ring",
198        feature = "native-tls"
199    ))]
200    #[error(transparent)]
201    Tls(#[from] anyhow::Error),
202    /// The URL to connect to carries no host.
203    #[cfg(any(
204        feature = "rustls-aws",
205        feature = "rustls-ring",
206        feature = "native-tls"
207    ))]
208    #[error("JMAP URL `{0}` has no host")]
209    UrlMissingHost(String),
210    /// The URL to connect to carries a scheme the client cannot open.
211    #[cfg(any(
212        feature = "rustls-aws",
213        feature = "rustls-ring",
214        feature = "native-tls"
215    ))]
216    #[error(
217        "JMAP URL `{url}` has unsupported scheme `{scheme}` (expected `http`, `https`, `jmap` or `jmaps`)"
218    )]
219    UrlUnsupportedScheme {
220        /// The URL the client was asked to open.
221        url: String,
222        /// The unsupported scheme of that URL.
223        scheme: String,
224    },
225    /// The server answered with a redirect during a non-redirectable
226    /// operation.
227    #[error("JMAP server redirected to `{0}` during a non-redirectable operation")]
228    UnexpectedRedirect(Url),
229    /// A method requiring the session ran before [`JmapClientStd::session_get`].
230    #[error("JMAP client missing session; call `session_get` first")]
231    MissingSession,
232}
233
234const READ_BUFFER_SIZE: usize = 16 * 1024;
235
236/// Std-blocking JMAP client wrapping a single boxed stream.
237pub struct JmapClientStd {
238    /// The wrapped stream the coroutines read from and write to.
239    pub stream: Box<dyn JmapStream>,
240    /// The pre-formatted HTTP `Authorization` header value.
241    pub http_auth: SecretString,
242    /// The session discovered by [`Self::session_get`], if any.
243    pub session: Option<JmapSession>,
244}
245
246impl JmapClientStd {
247    /// Builds a client around `stream`. The caller is responsible for opening
248    /// the connection (TCP, TLS handshake if needed) and for the bearer token /
249    /// authorization header value.
250    pub fn new<S: Read + Write + Send + 'static>(stream: S, http_auth: SecretString) -> Self {
251        Self {
252            stream: Box::new(stream),
253            http_auth,
254            session: None,
255        }
256    }
257
258    /// Default ALPN list for JMAP TLS handshakes: `["http/1.1"]` (JMAP rides
259    /// on HTTP/1.1). Exposed so config-based callers can share one source of
260    /// truth.
261    pub fn default_alpn() -> Vec<String> {
262        vec![String::from("http/1.1")]
263    }
264
265    /// Resumes any standard-shape coroutine (`Yield = JmapYield`) against the
266    /// wrapped stream until it terminates.
267    ///
268    /// Redirect-aware coroutines ([`JmapSessionGet`], [`JmapBlobUpload`],
269    /// [`JmapBlobDownload`]) and the streaming
270    /// [`JmapEventSource`](crate::rfc8620::event_source::subscribe::JmapEventSource)
271    /// have their own per-method loops.
272    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, JmapClientStdError>
273    where
274        C: JmapCoroutine<Yield = JmapYield, Return = Result<T, E>>,
275        JmapClientStdError: From<E>,
276    {
277        let mut buf = [0u8; READ_BUFFER_SIZE];
278        let mut arg: Option<&[u8]> = None;
279
280        loop {
281            match coroutine.resume(arg.take()) {
282                JmapCoroutineState::Complete(Ok(out)) => return Ok(out),
283                JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
284                JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
285                    let n = self.stream.read(&mut buf)?;
286                    arg = Some(&buf[..n]);
287                }
288                JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
289                    self.stream.write_all(&bytes)?;
290                    arg = None;
291                }
292            }
293        }
294    }
295
296    /// Builds a client from a pre-connected stream and an already-discovered
297    /// [`JmapSession`]. Skips [`Self::session_get`].
298    pub fn from_parts<S: Read + Write + Send + 'static>(
299        stream: S,
300        http_auth: SecretString,
301        session: JmapSession,
302    ) -> Self {
303        Self {
304            stream: Box::new(stream),
305            http_auth,
306            session: Some(session),
307        }
308    }
309
310    /// Connects to `url`, doing a TLS handshake for `https` / `jmaps` (plain
311    /// TCP for `http` / `jmap`). ALPN comes from `tls.rustls.alpn` (see
312    /// [`Self::default_alpn`]); empty vec skips ALPN.
313    #[cfg(any(
314        feature = "rustls-aws",
315        feature = "rustls-ring",
316        feature = "native-tls"
317    ))]
318    pub fn connect(
319        url: &Url,
320        tls: &Tls,
321        http_auth: SecretString,
322    ) -> Result<Self, JmapClientStdError> {
323        let host = url
324            .host_str()
325            .ok_or_else(|| JmapClientStdError::UrlMissingHost(url.to_string()))?;
326
327        let stream = match url.scheme() {
328            "http" | "jmap" => StreamStd::connect_tcp(host, url.port().unwrap_or(80))?,
329            "https" | "jmaps" => StreamStd::connect_tls(host, url.port().unwrap_or(443), tls)?,
330            scheme => {
331                return Err(JmapClientStdError::UrlUnsupportedScheme {
332                    url: url.to_string(),
333                    scheme: scheme.to_string(),
334                });
335            }
336        };
337
338        // NOTE: 5s per-read (not per-operation) timeout so the watch loop
339        // polls its shutdown atomic between SSE push frames; large JMAP
340        // responses keep working as long as TCP packets keep arriving.
341        stream.set_read_timeout(Some(Duration::from_secs(5)))?;
342
343        Ok(Self {
344            stream: Box::new(stream),
345            http_auth,
346            session: None,
347        })
348    }
349
350    /// Replaces the underlying stream; useful when `apiUrl`, `uploadUrl` or
351    /// `downloadUrl` resolves to a different authority than the first
352    /// connection target, or after a redirect.
353    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
354        self.stream = Box::new(stream);
355    }
356
357    /// Returns the cached session, if [`Self::session_get`] has run.
358    pub fn session(&self) -> Option<&JmapSession> {
359        self.session.as_ref()
360    }
361
362    /// Returns the pre-formatted HTTP `Authorization` header value.
363    pub fn http_auth(&self) -> &SecretString {
364        &self.http_auth
365    }
366
367    fn session_or_err(&self) -> Result<&JmapSession, JmapClientStdError> {
368        self.session
369            .as_ref()
370            .ok_or(JmapClientStdError::MissingSession)
371    }
372
373    /// Runs [`JmapSessionGet`] and caches the discovered session.
374    ///
375    /// `url` is either a base URL for `/.well-known/jmap` discovery or a
376    /// direct session endpoint. A 3xx response terminates with
377    /// [`JmapClientStdError::UnexpectedRedirect`].
378    pub fn session_get(&mut self, url: &Url) -> Result<&JmapSession, JmapClientStdError> {
379        let mut coroutine = JmapSessionGet::new(&self.http_auth, url);
380        let mut buf = [0u8; READ_BUFFER_SIZE];
381        let mut arg: Option<&[u8]> = None;
382
383        loop {
384            match coroutine.resume(arg.take()) {
385                JmapCoroutineState::Complete(Ok(JmapSessionGetOutput { session, .. })) => {
386                    self.session = Some(session);
387                    return Ok(self.session.as_ref().unwrap());
388                }
389                JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
390                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
391                    let n = self.stream.read(&mut buf)?;
392                    arg = Some(&buf[..n]);
393                }
394                JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
395                    self.stream.write_all(&bytes)?;
396                    arg = None;
397                }
398                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
399                    return Err(JmapClientStdError::UnexpectedRedirect(url));
400                }
401            }
402        }
403    }
404
405    /// Sends a raw JMAP request and returns the raw [`JmapResponse`]. Useful
406    /// for passthrough CLIs and ad-hoc requests with custom `using`
407    /// capabilities.
408    pub fn send_raw(&mut self, request: JmapRequest) -> Result<JmapResponse, JmapClientStdError> {
409        let session = self.session_or_err()?;
410        let coroutine = JmapSend::new(&self.http_auth, &session.api_url, request)?;
411        let out = self.run(coroutine)?;
412        Ok(out.response)
413    }
414
415    /// Uploads a blob to `upload_url` (RFC 8620 §6.1). The caller must resolve
416    /// the session's `uploadUrl` template (e.g. substitute `{accountId}`).
417    /// A 3xx response terminates with [`JmapClientStdError::UnexpectedRedirect`].
418    pub fn blob_upload(
419        &mut self,
420        upload_url: &Url,
421        content_type: &str,
422        data: Vec<u8>,
423    ) -> Result<JmapBlobUploadOutput, JmapClientStdError> {
424        let mut coroutine = JmapBlobUpload::new(&self.http_auth, upload_url, content_type, data);
425        let mut buf = [0u8; READ_BUFFER_SIZE];
426        let mut arg: Option<&[u8]> = None;
427
428        loop {
429            match coroutine.resume(arg.take()) {
430                JmapCoroutineState::Complete(Ok(out)) => return Ok(out),
431                JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
432                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
433                    let n = self.stream.read(&mut buf)?;
434                    arg = Some(&buf[..n]);
435                }
436                JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
437                    self.stream.write_all(&bytes)?;
438                    arg = None;
439                }
440                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
441                    return Err(JmapClientStdError::UnexpectedRedirect(url));
442                }
443            }
444        }
445    }
446
447    /// Downloads a blob from `download_url` (RFC 8620 §6.2). The caller must
448    /// resolve the session's `downloadUrl` template. A 3xx response terminates
449    /// with [`JmapClientStdError::UnexpectedRedirect`].
450    pub fn blob_download(&mut self, download_url: &Url) -> Result<Vec<u8>, JmapClientStdError> {
451        let mut coroutine = JmapBlobDownload::new(&self.http_auth, download_url);
452        let mut buf = [0u8; READ_BUFFER_SIZE];
453        let mut arg: Option<&[u8]> = None;
454
455        loop {
456            match coroutine.resume(arg.take()) {
457                JmapCoroutineState::Complete(Ok(out)) => return Ok(out.data),
458                JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
459                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
460                    let n = self.stream.read(&mut buf)?;
461                    arg = Some(&buf[..n]);
462                }
463                JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
464                    self.stream.write_all(&bytes)?;
465                    arg = None;
466                }
467                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
468                    return Err(JmapClientStdError::UnexpectedRedirect(url));
469                }
470            }
471        }
472    }
473
474    /// Runs [`JmapPushSubscriptionGet`] (`PushSubscription/get`).
475    pub fn push_subscription_get(
476        &mut self,
477        opts: JmapPushSubscriptionGetOptions,
478    ) -> Result<JmapPushSubscriptionGetOutput, JmapClientStdError> {
479        let coroutine =
480            JmapPushSubscriptionGet::new(self.session_or_err()?, &self.http_auth, opts)?;
481        self.run(coroutine)
482    }
483
484    /// Runs [`JmapPushSubscriptionSet`] (`PushSubscription/set`).
485    pub fn push_subscription_set(
486        &mut self,
487        args: JmapPushSubscriptionSetArgs,
488    ) -> Result<JmapPushSubscriptionSetOutput, JmapClientStdError> {
489        let coroutine =
490            JmapPushSubscriptionSet::new(self.session_or_err()?, &self.http_auth, args)?;
491        self.run(coroutine)
492    }
493
494    /// Runs [`JmapMailboxGet`] (`Mailbox/get`).
495    pub fn mailbox_get(
496        &mut self,
497        opts: JmapMailboxGetOptions,
498    ) -> Result<JmapMailboxGetOutput, JmapClientStdError> {
499        let coroutine = JmapMailboxGet::new(self.session_or_err()?, &self.http_auth, opts)?;
500        self.run(coroutine)
501    }
502
503    /// Runs [`JmapMailboxQuery`] (batched `Mailbox/query` +
504    /// `Mailbox/get`).
505    pub fn mailbox_query(
506        &mut self,
507        opts: JmapMailboxQueryOptions,
508    ) -> Result<JmapMailboxQueryOutput, JmapClientStdError> {
509        let coroutine = JmapMailboxQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
510        self.run(coroutine)
511    }
512
513    /// Runs [`JmapMailboxSet`] (`Mailbox/set`).
514    pub fn mailbox_set(
515        &mut self,
516        args: JmapMailboxSetArgs,
517    ) -> Result<JmapMailboxSetOutput, JmapClientStdError> {
518        let coroutine = JmapMailboxSet::new(self.session_or_err()?, &self.http_auth, args)?;
519        self.run(coroutine)
520    }
521
522    /// Runs [`JmapMailboxChanges`] (`Mailbox/changes`).
523    pub fn mailbox_changes(
524        &mut self,
525        since_state: impl Into<String>,
526        opts: JmapMailboxChangesOptions,
527    ) -> Result<JmapChangesOutput, JmapClientStdError> {
528        let coroutine =
529            JmapMailboxChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
530        self.run(coroutine)
531    }
532
533    /// Runs [`JmapEmailGet`] (`Email/get`).
534    pub fn email_get(
535        &mut self,
536        ids: Vec<String>,
537        opts: JmapEmailGetOptions,
538    ) -> Result<JmapEmailGetOutput, JmapClientStdError> {
539        let coroutine = JmapEmailGet::new(self.session_or_err()?, &self.http_auth, ids, opts)?;
540        self.run(coroutine)
541    }
542
543    /// Runs [`JmapEmailQuery`] (batched `Email/query` + `Email/get`).
544    pub fn email_query(
545        &mut self,
546        opts: JmapEmailQueryOptions,
547    ) -> Result<JmapEmailQueryOutput, JmapClientStdError> {
548        let coroutine = JmapEmailQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
549        self.run(coroutine)
550    }
551
552    /// Runs [`JmapEmailSet`] (`Email/set`).
553    pub fn email_set(
554        &mut self,
555        args: JmapEmailSetArgs,
556    ) -> Result<JmapEmailSetOutput, JmapClientStdError> {
557        let coroutine = JmapEmailSet::new(self.session_or_err()?, &self.http_auth, args)?;
558        self.run(coroutine)
559    }
560
561    /// Runs [`JmapEmailChanges`] (`Email/changes`).
562    pub fn email_changes(
563        &mut self,
564        since_state: impl Into<String>,
565        opts: JmapEmailChangesOptions,
566    ) -> Result<JmapChangesOutput, JmapClientStdError> {
567        let coroutine =
568            JmapEmailChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
569        self.run(coroutine)
570    }
571
572    /// Runs [`JmapEmailCopy`] (`Email/copy`).
573    pub fn email_copy(
574        &mut self,
575        from_account_id: impl Into<String>,
576        emails: BTreeMap<String, JmapEmailCopyArgs>,
577    ) -> Result<JmapEmailCopyOutput, JmapClientStdError> {
578        let coroutine = JmapEmailCopy::new(
579            self.session_or_err()?,
580            &self.http_auth,
581            from_account_id,
582            emails,
583        )?;
584        self.run(coroutine)
585    }
586
587    /// Runs [`JmapEmailImport`] (`Email/import`).
588    pub fn email_import(
589        &mut self,
590        emails: BTreeMap<String, JmapEmailImportArgs>,
591    ) -> Result<JmapEmailImportOutput, JmapClientStdError> {
592        let coroutine = JmapEmailImport::new(self.session_or_err()?, &self.http_auth, emails)?;
593        self.run(coroutine)
594    }
595
596    /// Runs [`JmapEmailParse`] (`Email/parse`).
597    pub fn email_parse(
598        &mut self,
599        blob_ids: Vec<String>,
600        opts: JmapEmailParseOptions,
601    ) -> Result<JmapEmailParseOutput, JmapClientStdError> {
602        let coroutine =
603            JmapEmailParse::new(self.session_or_err()?, &self.http_auth, blob_ids, opts)?;
604        self.run(coroutine)
605    }
606
607    /// Runs [`JmapThreadGet`] (`Thread/get`).
608    pub fn thread_get(
609        &mut self,
610        ids: Vec<String>,
611    ) -> Result<JmapThreadGetOutput, JmapClientStdError> {
612        let coroutine = JmapThreadGet::new(self.session_or_err()?, &self.http_auth, ids)?;
613        self.run(coroutine)
614    }
615
616    /// Runs [`JmapThreadChanges`] (`Thread/changes`).
617    pub fn thread_changes(
618        &mut self,
619        since_state: impl Into<String>,
620        opts: JmapThreadChangesOptions,
621    ) -> Result<JmapChangesOutput, JmapClientStdError> {
622        let coroutine =
623            JmapThreadChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
624        self.run(coroutine)
625    }
626
627    /// Runs [`JmapIdentityGet`] (`Identity/get`).
628    pub fn identity_get(
629        &mut self,
630        opts: JmapIdentityGetOptions,
631    ) -> Result<JmapIdentityGetOutput, JmapClientStdError> {
632        let coroutine = JmapIdentityGet::new(self.session_or_err()?, &self.http_auth, opts)?;
633        self.run(coroutine)
634    }
635
636    /// Runs [`JmapIdentitySet`] (`Identity/set`).
637    pub fn identity_set(
638        &mut self,
639        args: JmapIdentitySetArgs,
640    ) -> Result<JmapIdentitySetOutput, JmapClientStdError> {
641        let coroutine = JmapIdentitySet::new(self.session_or_err()?, &self.http_auth, args)?;
642        self.run(coroutine)
643    }
644
645    /// Runs [`JmapEmailSubmissionGet`] (`EmailSubmission/get`).
646    pub fn email_submission_get(
647        &mut self,
648        opts: JmapEmailSubmissionGetOptions,
649    ) -> Result<JmapEmailSubmissionGetOutput, JmapClientStdError> {
650        let coroutine = JmapEmailSubmissionGet::new(self.session_or_err()?, &self.http_auth, opts)?;
651        self.run(coroutine)
652    }
653
654    /// Runs [`JmapEmailSubmissionQuery`] (batched
655    /// `EmailSubmission/query` + `EmailSubmission/get`).
656    pub fn email_submission_query(
657        &mut self,
658        opts: JmapEmailSubmissionQueryOptions,
659    ) -> Result<JmapEmailSubmissionQueryOutput, JmapClientStdError> {
660        let coroutine =
661            JmapEmailSubmissionQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
662        self.run(coroutine)
663    }
664
665    /// Runs [`JmapEmailSubmissionSet`] (`EmailSubmission/set`).
666    pub fn email_submission_set(
667        &mut self,
668        submissions: BTreeMap<String, JmapEmailSubmissionCreate>,
669    ) -> Result<JmapEmailSubmissionSetOutput, JmapClientStdError> {
670        let coroutine =
671            JmapEmailSubmissionSet::new(self.session_or_err()?, &self.http_auth, submissions)?;
672        self.run(coroutine)
673    }
674
675    /// Runs [`JmapEmailSubmissionCancel`] (`EmailSubmission/set` with
676    /// `undoStatus: "canceled"`).
677    pub fn email_submission_cancel(
678        &mut self,
679        ids: Vec<String>,
680    ) -> Result<JmapEmailSubmissionCancelOutput, JmapClientStdError> {
681        let coroutine =
682            JmapEmailSubmissionCancel::new(self.session_or_err()?, &self.http_auth, ids)?;
683        self.run(coroutine)
684    }
685
686    /// Runs [`JmapVacationResponseGet`]; returns the singleton, if any.
687    pub fn vacation_response_get(
688        &mut self,
689    ) -> Result<Option<JmapVacationResponse>, JmapClientStdError> {
690        let coroutine = JmapVacationResponseGet::new(self.session_or_err()?, &self.http_auth)?;
691        Ok(self.run(coroutine)?.vacation_response)
692    }
693
694    /// Runs [`JmapVacationResponseSet`]; returns the updated singleton if the
695    /// server echoed it back.
696    pub fn vacation_response_set(
697        &mut self,
698        patch: JmapVacationResponseUpdate,
699    ) -> Result<Option<JmapVacationResponse>, JmapClientStdError> {
700        let coroutine =
701            JmapVacationResponseSet::new(self.session_or_err()?, &self.http_auth, patch)?;
702        Ok(self.run(coroutine)?.updated)
703    }
704
705    /// Runs [`JmapAddressBookGet`] (`AddressBook/get`).
706    pub fn address_book_get(
707        &mut self,
708        opts: JmapAddressBookGetOptions,
709    ) -> Result<JmapAddressBookGetOutput, JmapClientStdError> {
710        let coroutine = JmapAddressBookGet::new(self.session_or_err()?, &self.http_auth, opts)?;
711        self.run(coroutine)
712    }
713
714    /// Runs [`JmapAddressBookSet`] (`AddressBook/set`).
715    pub fn address_book_set(
716        &mut self,
717        args: JmapAddressBookSetArgs,
718    ) -> Result<JmapAddressBookSetOutput, JmapClientStdError> {
719        let coroutine = JmapAddressBookSet::new(self.session_or_err()?, &self.http_auth, args)?;
720        self.run(coroutine)
721    }
722
723    /// Runs [`JmapAddressBookChanges`] (`AddressBook/changes`).
724    pub fn address_book_changes(
725        &mut self,
726        since_state: impl Into<String>,
727        opts: JmapAddressBookChangesOptions,
728    ) -> Result<JmapChangesOutput, JmapClientStdError> {
729        let coroutine = JmapAddressBookChanges::new(
730            self.session_or_err()?,
731            &self.http_auth,
732            since_state,
733            opts,
734        )?;
735        self.run(coroutine)
736    }
737
738    /// Runs [`JmapContactCardGet`] (`ContactCard/get`).
739    pub fn contact_card_get(
740        &mut self,
741        opts: JmapContactCardGetOptions,
742    ) -> Result<JmapContactCardGetOutput, JmapClientStdError> {
743        let coroutine = JmapContactCardGet::new(self.session_or_err()?, &self.http_auth, opts)?;
744        self.run(coroutine)
745    }
746
747    /// Runs [`JmapContactCardQuery`] (batched `ContactCard/query` +
748    /// `ContactCard/get`).
749    pub fn contact_card_query(
750        &mut self,
751        opts: JmapContactCardQueryOptions,
752    ) -> Result<JmapContactCardQueryOutput, JmapClientStdError> {
753        let coroutine = JmapContactCardQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
754        self.run(coroutine)
755    }
756
757    /// Runs [`JmapContactCardSet`] (`ContactCard/set`).
758    pub fn contact_card_set(
759        &mut self,
760        args: JmapContactCardSetArgs,
761    ) -> Result<JmapContactCardSetOutput, JmapClientStdError> {
762        let coroutine = JmapContactCardSet::new(self.session_or_err()?, &self.http_auth, args)?;
763        self.run(coroutine)
764    }
765
766    /// Runs [`JmapContactCardChanges`] (`ContactCard/changes`).
767    pub fn contact_card_changes(
768        &mut self,
769        since_state: impl Into<String>,
770        opts: JmapContactCardChangesOptions,
771    ) -> Result<JmapChangesOutput, JmapClientStdError> {
772        let coroutine = JmapContactCardChanges::new(
773            self.session_or_err()?,
774            &self.http_auth,
775            since_state,
776            opts,
777        )?;
778        self.run(coroutine)
779    }
780
781    /// Runs [`JmapContactCardCopy`] (`ContactCard/copy`).
782    pub fn contact_card_copy(
783        &mut self,
784        from_account_id: impl Into<String>,
785        cards: BTreeMap<String, JmapContactCardCopyArgs>,
786    ) -> Result<JmapContactCardCopyOutput, JmapClientStdError> {
787        let coroutine = JmapContactCardCopy::new(
788            self.session_or_err()?,
789            &self.http_auth,
790            from_account_id,
791            cards,
792        )?;
793        self.run(coroutine)
794    }
795}
796
797impl fmt::Debug for JmapClientStd {
798    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
799        f.debug_struct("JmapClientStd")
800            .field("http_auth", &self.http_auth)
801            .field("session", &self.session)
802            .finish_non_exhaustive()
803    }
804}
805
806/// Erased stream the client resumes coroutines against: auto-implemented for
807/// any blocking `Read + Write + Send + 'static`. `Send` flows through the
808/// `Box<dyn …>` so [`JmapClientStd`] can move between worker threads;
809/// [`Self::as_any_mut`] lets specialized callers downcast back to the
810/// concrete stream.
811pub trait JmapStream: Read + Write + Send + Any {
812    /// Upcasts the stream to [`Any`] for downcasting to the concrete type.
813    fn as_any_mut(&mut self) -> &mut dyn Any;
814}
815
816impl<T: Read + Write + Send + Any> JmapStream for T {
817    fn as_any_mut(&mut self) -> &mut dyn Any {
818        self
819    }
820}