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