1use core::{any::Any, fmt, time::Duration};
29
30#[cfg(any(
31 feature = "rustls-aws",
32 feature = "rustls-ring",
33 feature = "native-tls"
34))]
35use alloc::string::ToString;
36use alloc::{boxed::Box, collections::BTreeMap, string::String, vec, vec::Vec};
37
38use std::io::{self, Read, Write};
39
40#[cfg(any(
41 feature = "rustls-aws",
42 feature = "rustls-ring",
43 feature = "native-tls"
44))]
45use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
46use secrecy::SecretString;
47use thiserror::Error;
48use url::Url;
49
50use crate::{
51 coroutine::*,
52 rfc8620::{
53 JmapRequest, JmapResponse, JmapSession, blob_download::*, blob_upload::*,
54 changes::JmapChangesOutput, coroutine::JmapRedirectYield, send::*, session_get::*,
55 },
56 rfc8621::{
57 email::{
58 JmapEmailCopyArgs, JmapEmailImportArgs, changes::*, copy::*, get::*, import::*,
59 parse::*, query::*, set::*,
60 },
61 email_submission::{cancel::*, get::*, query::*, set::*, *},
62 identity::{get::*, set::*},
63 mailbox::{changes::*, get::*, query::*, set::*},
64 thread::{changes::*, get::*},
65 vacation_response::{get::*, set::*, *},
66 },
67};
68
69#[derive(Debug, Error)]
71pub enum JmapClientStdError {
72 #[error(transparent)]
73 Send(#[from] JmapSendError),
74 #[error(transparent)]
75 SessionGet(#[from] JmapSessionGetError),
76 #[error(transparent)]
77 BlobUpload(#[from] JmapBlobUploadError),
78 #[error(transparent)]
79 BlobDownload(#[from] JmapBlobDownloadError),
80
81 #[error(transparent)]
82 MailboxGet(#[from] JmapMailboxGetError),
83 #[error(transparent)]
84 MailboxQuery(#[from] JmapMailboxQueryError),
85 #[error(transparent)]
86 MailboxSet(#[from] JmapMailboxSetError),
87 #[error(transparent)]
88 MailboxChanges(#[from] JmapMailboxChangesError),
89
90 #[error(transparent)]
91 EmailGet(#[from] JmapEmailGetError),
92 #[error(transparent)]
93 EmailQuery(#[from] JmapEmailQueryError),
94 #[error(transparent)]
95 EmailSet(#[from] JmapEmailSetError),
96 #[error(transparent)]
97 EmailChanges(#[from] JmapEmailChangesError),
98 #[error(transparent)]
99 JmapEmailCopyArgs(#[from] JmapEmailCopyError),
100 #[error(transparent)]
101 JmapEmailImportArgs(#[from] JmapEmailImportError),
102 #[error(transparent)]
103 EmailParse(#[from] JmapEmailParseError),
104
105 #[error(transparent)]
106 ThreadGet(#[from] JmapThreadGetError),
107 #[error(transparent)]
108 ThreadChanges(#[from] JmapThreadChangesError),
109
110 #[error(transparent)]
111 IdentityGet(#[from] JmapIdentityGetError),
112 #[error(transparent)]
113 IdentitySet(#[from] JmapIdentitySetError),
114
115 #[error(transparent)]
116 EmailSubmissionGet(#[from] JmapEmailSubmissionGetError),
117 #[error(transparent)]
118 EmailSubmissionQuery(#[from] JmapEmailSubmissionQueryError),
119 #[error(transparent)]
120 EmailSubmissionSet(#[from] JmapEmailSubmissionSetError),
121 #[error(transparent)]
122 EmailSubmissionCancel(#[from] JmapEmailSubmissionCancelError),
123
124 #[error(transparent)]
125 VacationResponseGet(#[from] JmapVacationResponseGetError),
126 #[error(transparent)]
127 VacationResponseSet(#[from] JmapVacationResponseSetError),
128
129 #[error(transparent)]
130 Io(#[from] io::Error),
131
132 #[cfg(any(
133 feature = "rustls-aws",
134 feature = "rustls-ring",
135 feature = "native-tls"
136 ))]
137 #[error(transparent)]
138 Tls(#[from] anyhow::Error),
139 #[cfg(any(
140 feature = "rustls-aws",
141 feature = "rustls-ring",
142 feature = "native-tls"
143 ))]
144 #[error("JMAP URL `{0}` has no host")]
145 UrlMissingHost(String),
146 #[cfg(any(
147 feature = "rustls-aws",
148 feature = "rustls-ring",
149 feature = "native-tls"
150 ))]
151 #[error(
152 "JMAP URL `{0}` has unsupported scheme `{1}` (expected `http`, `https`, `jmap` or `jmaps`)"
153 )]
154 UrlUnsupportedScheme(String, String),
155
156 #[error("JMAP server redirected to `{0}` during a non-redirectable operation")]
157 UnexpectedRedirect(Url),
158 #[error("JMAP client missing session; call `session_get` first")]
159 MissingSession,
160}
161
162const READ_BUFFER_SIZE: usize = 16 * 1024;
163
164pub fn default_alpn() -> Vec<String> {
167 vec![String::from("http/1.1")]
168}
169
170pub struct JmapClientStd {
172 pub stream: Box<dyn JmapStream>,
173 pub http_auth: SecretString,
174 pub session: Option<JmapSession>,
175}
176
177impl JmapClientStd {
178 pub fn new<S: Read + Write + Send + 'static>(stream: S, http_auth: SecretString) -> Self {
182 Self {
183 stream: Box::new(stream),
184 http_auth,
185 session: None,
186 }
187 }
188
189 pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, JmapClientStdError>
197 where
198 C: JmapCoroutine<Yield = JmapYield, Return = Result<T, E>>,
199 JmapClientStdError: From<E>,
200 {
201 let mut buf = [0u8; READ_BUFFER_SIZE];
202 let mut arg: Option<&[u8]> = None;
203
204 loop {
205 match coroutine.resume(arg.take()) {
206 JmapCoroutineState::Complete(Ok(out)) => return Ok(out),
207 JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
208 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
209 let n = self.stream.read(&mut buf)?;
210 arg = Some(&buf[..n]);
211 }
212 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
213 self.stream.write_all(&bytes)?;
214 arg = None;
215 }
216 }
217 }
218 }
219
220 pub fn from_parts<S: Read + Write + Send + 'static>(
223 stream: S,
224 http_auth: SecretString,
225 session: JmapSession,
226 ) -> Self {
227 Self {
228 stream: Box::new(stream),
229 http_auth,
230 session: Some(session),
231 }
232 }
233
234 #[cfg(any(
238 feature = "rustls-aws",
239 feature = "rustls-ring",
240 feature = "native-tls"
241 ))]
242 pub fn connect(
243 url: &Url,
244 tls: &Tls,
245 http_auth: SecretString,
246 ) -> Result<Self, JmapClientStdError> {
247 let host = url
248 .host_str()
249 .ok_or_else(|| JmapClientStdError::UrlMissingHost(url.to_string()))?;
250
251 let stream = match url.scheme() {
252 "http" | "jmap" => StreamStd::connect_tcp(host, url.port().unwrap_or(80))?,
253 "https" | "jmaps" => StreamStd::connect_tls(host, url.port().unwrap_or(443), tls)?,
254 scheme => {
255 return Err(JmapClientStdError::UrlUnsupportedScheme(
256 url.to_string(),
257 scheme.to_string(),
258 ));
259 }
260 };
261
262 stream.set_read_timeout(Some(Duration::from_secs(5)))?;
266
267 Ok(Self {
268 stream: Box::new(stream),
269 http_auth,
270 session: None,
271 })
272 }
273
274 pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
278 self.stream = Box::new(stream);
279 }
280
281 pub fn session(&self) -> Option<&JmapSession> {
283 self.session.as_ref()
284 }
285
286 pub fn http_auth(&self) -> &SecretString {
288 &self.http_auth
289 }
290
291 fn session_or_err(&self) -> Result<&JmapSession, JmapClientStdError> {
292 self.session
293 .as_ref()
294 .ok_or(JmapClientStdError::MissingSession)
295 }
296
297 pub fn session_get(&mut self, url: &Url) -> Result<&JmapSession, JmapClientStdError> {
303 let mut coroutine = JmapSessionGet::new(&self.http_auth, url);
304 let mut buf = [0u8; READ_BUFFER_SIZE];
305 let mut arg: Option<&[u8]> = None;
306
307 loop {
308 match coroutine.resume(arg.take()) {
309 JmapCoroutineState::Complete(Ok(JmapSessionGetOutput { session, .. })) => {
310 self.session = Some(session);
311 return Ok(self.session.as_ref().unwrap());
312 }
313 JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
314 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
315 let n = self.stream.read(&mut buf)?;
316 arg = Some(&buf[..n]);
317 }
318 JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
319 self.stream.write_all(&bytes)?;
320 arg = None;
321 }
322 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
323 return Err(JmapClientStdError::UnexpectedRedirect(url));
324 }
325 }
326 }
327 }
328
329 pub fn send_raw(&mut self, request: JmapRequest) -> Result<JmapResponse, JmapClientStdError> {
334 let session = self.session_or_err()?;
335 let coroutine = JmapSend::new(&self.http_auth, &session.api_url, request)?;
336 let out = self.run(coroutine)?;
337 Ok(out.response)
338 }
339
340 pub fn blob_upload(
346 &mut self,
347 upload_url: &Url,
348 content_type: &str,
349 data: Vec<u8>,
350 ) -> Result<JmapBlobUploadOutput, JmapClientStdError> {
351 let mut coroutine = JmapBlobUpload::new(&self.http_auth, upload_url, content_type, data);
352 let mut buf = [0u8; READ_BUFFER_SIZE];
353 let mut arg: Option<&[u8]> = None;
354
355 loop {
356 match coroutine.resume(arg.take()) {
357 JmapCoroutineState::Complete(Ok(out)) => return Ok(out),
358 JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
359 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
360 let n = self.stream.read(&mut buf)?;
361 arg = Some(&buf[..n]);
362 }
363 JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
364 self.stream.write_all(&bytes)?;
365 arg = None;
366 }
367 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
368 return Err(JmapClientStdError::UnexpectedRedirect(url));
369 }
370 }
371 }
372 }
373
374 pub fn blob_download(&mut self, download_url: &Url) -> Result<Vec<u8>, JmapClientStdError> {
378 let mut coroutine = JmapBlobDownload::new(&self.http_auth, download_url);
379 let mut buf = [0u8; READ_BUFFER_SIZE];
380 let mut arg: Option<&[u8]> = None;
381
382 loop {
383 match coroutine.resume(arg.take()) {
384 JmapCoroutineState::Complete(Ok(out)) => return Ok(out.data),
385 JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
386 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
387 let n = self.stream.read(&mut buf)?;
388 arg = Some(&buf[..n]);
389 }
390 JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
391 self.stream.write_all(&bytes)?;
392 arg = None;
393 }
394 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
395 return Err(JmapClientStdError::UnexpectedRedirect(url));
396 }
397 }
398 }
399 }
400
401 pub fn mailbox_get(
405 &mut self,
406 opts: JmapMailboxGetOptions,
407 ) -> Result<JmapMailboxGetOutput, JmapClientStdError> {
408 let coroutine = JmapMailboxGet::new(self.session_or_err()?, &self.http_auth, opts)?;
409 self.run(coroutine)
410 }
411
412 pub fn mailbox_query(
415 &mut self,
416 opts: JmapMailboxQueryOptions,
417 ) -> Result<JmapMailboxQueryOutput, JmapClientStdError> {
418 let coroutine = JmapMailboxQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
419 self.run(coroutine)
420 }
421
422 pub fn mailbox_set(
424 &mut self,
425 args: JmapMailboxSetArgs,
426 ) -> Result<JmapMailboxSetOutput, JmapClientStdError> {
427 let coroutine = JmapMailboxSet::new(self.session_or_err()?, &self.http_auth, args)?;
428 self.run(coroutine)
429 }
430
431 pub fn mailbox_changes(
433 &mut self,
434 since_state: impl Into<String>,
435 opts: JmapMailboxChangesOptions,
436 ) -> Result<JmapChangesOutput, JmapClientStdError> {
437 let coroutine =
438 JmapMailboxChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
439 self.run(coroutine)
440 }
441
442 pub fn email_get(
446 &mut self,
447 ids: Vec<String>,
448 opts: JmapEmailGetOptions,
449 ) -> Result<JmapEmailGetOutput, JmapClientStdError> {
450 let coroutine = JmapEmailGet::new(self.session_or_err()?, &self.http_auth, ids, opts)?;
451 self.run(coroutine)
452 }
453
454 pub fn email_query(
456 &mut self,
457 opts: JmapEmailQueryOptions,
458 ) -> Result<JmapEmailQueryOutput, JmapClientStdError> {
459 let coroutine = JmapEmailQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
460 self.run(coroutine)
461 }
462
463 pub fn email_set(
465 &mut self,
466 args: JmapEmailSetArgs,
467 ) -> Result<JmapEmailSetOutput, JmapClientStdError> {
468 let coroutine = JmapEmailSet::new(self.session_or_err()?, &self.http_auth, args)?;
469 self.run(coroutine)
470 }
471
472 pub fn email_changes(
474 &mut self,
475 since_state: impl Into<String>,
476 opts: JmapEmailChangesOptions,
477 ) -> Result<JmapChangesOutput, JmapClientStdError> {
478 let coroutine =
479 JmapEmailChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
480 self.run(coroutine)
481 }
482
483 pub fn email_copy(
485 &mut self,
486 from_account_id: impl Into<String>,
487 emails: BTreeMap<String, JmapEmailCopyArgs>,
488 ) -> Result<JmapEmailCopyOutput, JmapClientStdError> {
489 let coroutine = JmapEmailCopy::new(
490 self.session_or_err()?,
491 &self.http_auth,
492 from_account_id,
493 emails,
494 )?;
495 self.run(coroutine)
496 }
497
498 pub fn email_import(
500 &mut self,
501 emails: BTreeMap<String, JmapEmailImportArgs>,
502 ) -> Result<JmapEmailImportOutput, JmapClientStdError> {
503 let coroutine = JmapEmailImport::new(self.session_or_err()?, &self.http_auth, emails)?;
504 self.run(coroutine)
505 }
506
507 pub fn email_parse(
509 &mut self,
510 blob_ids: Vec<String>,
511 opts: JmapEmailParseOptions,
512 ) -> Result<JmapEmailParseOutput, JmapClientStdError> {
513 let coroutine =
514 JmapEmailParse::new(self.session_or_err()?, &self.http_auth, blob_ids, opts)?;
515 self.run(coroutine)
516 }
517
518 pub fn thread_get(
522 &mut self,
523 ids: Vec<String>,
524 ) -> Result<JmapThreadGetOutput, JmapClientStdError> {
525 let coroutine = JmapThreadGet::new(self.session_or_err()?, &self.http_auth, ids)?;
526 self.run(coroutine)
527 }
528
529 pub fn thread_changes(
531 &mut self,
532 since_state: impl Into<String>,
533 opts: JmapThreadChangesOptions,
534 ) -> Result<JmapChangesOutput, JmapClientStdError> {
535 let coroutine =
536 JmapThreadChanges::new(self.session_or_err()?, &self.http_auth, since_state, opts)?;
537 self.run(coroutine)
538 }
539
540 pub fn identity_get(
544 &mut self,
545 opts: JmapIdentityGetOptions,
546 ) -> Result<JmapIdentityGetOutput, JmapClientStdError> {
547 let coroutine = JmapIdentityGet::new(self.session_or_err()?, &self.http_auth, opts)?;
548 self.run(coroutine)
549 }
550
551 pub fn identity_set(
553 &mut self,
554 args: JmapIdentitySetArgs,
555 ) -> Result<JmapIdentitySetOutput, JmapClientStdError> {
556 let coroutine = JmapIdentitySet::new(self.session_or_err()?, &self.http_auth, args)?;
557 self.run(coroutine)
558 }
559
560 pub fn email_submission_get(
564 &mut self,
565 opts: JmapEmailSubmissionGetOptions,
566 ) -> Result<JmapEmailSubmissionGetOutput, JmapClientStdError> {
567 let coroutine = JmapEmailSubmissionGet::new(self.session_or_err()?, &self.http_auth, opts)?;
568 self.run(coroutine)
569 }
570
571 pub fn email_submission_query(
574 &mut self,
575 opts: JmapEmailSubmissionQueryOptions,
576 ) -> Result<JmapEmailSubmissionQueryOutput, JmapClientStdError> {
577 let coroutine =
578 JmapEmailSubmissionQuery::new(self.session_or_err()?, &self.http_auth, opts)?;
579 self.run(coroutine)
580 }
581
582 pub fn email_submission_set(
584 &mut self,
585 submissions: BTreeMap<String, JmapEmailSubmissionCreate>,
586 ) -> Result<JmapEmailSubmissionSetOutput, JmapClientStdError> {
587 let coroutine =
588 JmapEmailSubmissionSet::new(self.session_or_err()?, &self.http_auth, submissions)?;
589 self.run(coroutine)
590 }
591
592 pub fn email_submission_cancel(
595 &mut self,
596 ids: Vec<String>,
597 ) -> Result<JmapEmailSubmissionCancelOutput, JmapClientStdError> {
598 let coroutine =
599 JmapEmailSubmissionCancel::new(self.session_or_err()?, &self.http_auth, ids)?;
600 self.run(coroutine)
601 }
602
603 pub fn vacation_response_get(
607 &mut self,
608 ) -> Result<Option<JmapVacationResponse>, JmapClientStdError> {
609 let coroutine = JmapVacationResponseGet::new(self.session_or_err()?, &self.http_auth)?;
610 Ok(self.run(coroutine)?.vacation_response)
611 }
612
613 pub fn vacation_response_set(
616 &mut self,
617 patch: JmapVacationResponseUpdate,
618 ) -> Result<Option<JmapVacationResponse>, JmapClientStdError> {
619 let coroutine =
620 JmapVacationResponseSet::new(self.session_or_err()?, &self.http_auth, patch)?;
621 Ok(self.run(coroutine)?.updated)
622 }
623}
624
625impl fmt::Debug for JmapClientStd {
626 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627 f.debug_struct("JmapClientStd")
628 .field("http_auth", &self.http_auth)
629 .field("session", &self.session)
630 .finish_non_exhaustive()
631 }
632}
633
634pub trait JmapStream: Read + Write + Send + Any {
639 fn as_any_mut(&mut self) -> &mut dyn Any;
640}
641
642impl<T: Read + Write + Send + Any> JmapStream for T {
643 fn as_any_mut(&mut self) -> &mut dyn Any {
644 self
645 }
646}