1use alloc::{string::String, sync::Arc, vec::Vec};
20use core::sync::atomic::AtomicBool;
21use std::{
22 io::{self, ErrorKind, Read, Write},
23 sync::mpsc::Sender,
24};
25
26use io_jmap::{
27 client::{JmapClientStd as InnerJmapClientStd, JmapClientStdError as InnerJmapClientStdError},
28 coroutine::*,
29 rfc8620::{JmapMethodError, JmapSession, changes::JmapChangesError},
30 rfc8621::{
31 email::{changes::JmapEmailChangesOptions, get::JmapEmailGetOptions},
32 mailbox::{
33 changes::{JmapMailboxChangesError, JmapMailboxChangesOptions},
34 get::JmapMailboxGetOptions,
35 },
36 },
37};
38#[cfg(any(
39 feature = "rustls-ring",
40 feature = "rustls-aws",
41 feature = "native-tls"
42))]
43use pimalaya_stream::tls::Tls;
44use secrecy::SecretString;
45use thiserror::Error;
46use url::Url;
47
48#[cfg(feature = "search")]
49use crate::{
50 envelope::jmap::search::{JmapEnvelopeSearch, JmapEnvelopeSearchError},
51 search::query::SearchEmailsQuery,
52};
53use crate::{
54 envelope::{
55 event::WatchEvent,
56 jmap::{
57 diff as envelope_diff,
58 list::{JmapEnvelopeList, JmapEnvelopeListError},
59 watch::{JmapWatchMailbox, JmapWatchMailboxError, JmapWatchMailboxYield},
60 },
61 types::{Envelope, EnvelopeDiff, FlagUpdate},
62 },
63 flag::{
64 jmap::store::{JmapFlagStore, JmapFlagStoreError},
65 types::{Flag, FlagOp},
66 },
67 jmap::convert::{envelope_from, envelope_properties},
68 mailbox::{
69 jmap::{
70 create::{JmapMailboxCreate, JmapMailboxCreateError},
71 delete::{JmapMailboxDelete, JmapMailboxDeleteError},
72 list::{JmapMailboxList, JmapMailboxListError},
73 },
74 types::{Mailbox, MailboxDiff},
75 },
76 message::jmap::{
77 add::{JmapMessageAdd, JmapMessageAddError},
78 copy::{JmapMessageCopy, JmapMessageCopyError},
79 delete::{JmapMessageDelete, JmapMessageDeleteError},
80 get::{JmapMessageGet, JmapMessageGetError},
81 r#move::{JmapMessageMove, JmapMessageMoveError},
82 send::{JmapMessageSend, JmapMessageSendError},
83 },
84};
85
86#[derive(Debug, Error)]
90pub enum JmapClientError {
91 #[error(transparent)]
92 Io(#[from] io::Error),
93 #[error("JMAP session is not initialised; call connect or session_get first")]
94 MissingSession,
95 #[error(transparent)]
96 MailboxList(#[from] JmapMailboxListError),
97 #[error(transparent)]
98 EnvelopeList(#[from] JmapEnvelopeListError),
99 #[cfg(feature = "search")]
100 #[error(transparent)]
101 EnvelopeSearch(#[from] JmapEnvelopeSearchError),
102 #[error(transparent)]
103 FlagStore(#[from] JmapFlagStoreError),
104 #[error(transparent)]
105 MailboxCreate(#[from] JmapMailboxCreateError),
106 #[error(transparent)]
107 MailboxDelete(#[from] JmapMailboxDeleteError),
108 #[error(transparent)]
109 MessageAdd(#[from] JmapMessageAddError),
110 #[error(transparent)]
111 MessageCopy(#[from] JmapMessageCopyError),
112 #[error(transparent)]
113 MessageDelete(#[from] JmapMessageDeleteError),
114 #[error(transparent)]
115 MessageGet(#[from] JmapMessageGetError),
116 #[error(transparent)]
117 MessageMove(#[from] JmapMessageMoveError),
118 #[error(transparent)]
119 MessageSend(#[from] JmapMessageSendError),
120 #[error(transparent)]
121 WatchMailbox(#[from] JmapWatchMailboxError),
122 #[error(transparent)]
123 Inner(#[from] InnerJmapClientStdError),
124}
125
126const READ_BUFFER_SIZE: usize = 16 * 1024;
127
128pub struct JmapClientStd {
139 pub inner: InnerJmapClientStd,
140 pub identity_id: Option<String>,
141 pub drafts_mailbox_id: Option<String>,
142}
143
144impl JmapClientStd {
145 pub fn new<S: Read + Write + Send + 'static>(stream: S, http_auth: SecretString) -> Self {
149 Self {
150 inner: InnerJmapClientStd::new(stream, http_auth),
151 identity_id: None,
152 drafts_mailbox_id: None,
153 }
154 }
155
156 pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, JmapClientError>
164 where
165 C: JmapCoroutine<Yield = JmapYield, Return = Result<T, E>>,
166 JmapClientError: From<E>,
167 {
168 let mut buf = [0u8; READ_BUFFER_SIZE];
169 let mut arg: Option<&[u8]> = None;
170
171 loop {
172 match coroutine.resume(arg.take()) {
173 JmapCoroutineState::Complete(Ok(out)) => return Ok(out),
174 JmapCoroutineState::Complete(Err(err)) => return Err(err.into()),
175 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
176 let n = self.inner.stream.read(&mut buf)?;
177 arg = Some(&buf[..n]);
178 }
179 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
180 self.inner.stream.write_all(&bytes)?;
181 }
182 }
183 }
184 }
185
186 pub fn session_get(&mut self, url: &Url) -> Result<(), JmapClientError> {
190 self.inner.session_get(url)?;
191 Ok(())
192 }
193
194 pub fn list_mailboxes(&mut self, with_counts: bool) -> Result<Vec<Mailbox>, JmapClientError> {
199 let coroutine = {
200 let session = self.session_or_err()?;
201 let http_auth = &self.inner.http_auth;
202 JmapMailboxList::new(session, http_auth, with_counts)?
203 };
204 self.run(coroutine)
205 }
206
207 pub fn list_envelopes(
210 &mut self,
211 mailbox: &str,
212 page: Option<u32>,
213 page_size: Option<u32>,
214 ) -> Result<Vec<Envelope>, JmapClientError> {
215 let coroutine = {
216 let session = self.session_or_err()?;
217 let http_auth = &self.inner.http_auth;
218 JmapEnvelopeList::new(session, http_auth, mailbox, page, page_size)?
219 };
220 self.run(coroutine)
221 }
222
223 #[cfg(feature = "search")]
228 pub fn search_envelopes(
229 &mut self,
230 mailbox: &str,
231 query: Option<&SearchEmailsQuery>,
232 page: Option<u32>,
233 page_size: Option<u32>,
234 ) -> Result<Vec<Envelope>, JmapClientError> {
235 let coroutine = {
236 let session = self.session_or_err()?;
237 let http_auth = &self.inner.http_auth;
238 JmapEnvelopeSearch::new(session, http_auth, mailbox, query, page, page_size)?
239 };
240 self.run(coroutine)
241 }
242
243 pub fn store_flags(
247 &mut self,
248 mailbox: &str,
249 ids: &[&str],
250 flags: &[Flag],
251 op: FlagOp,
252 ) -> Result<(), JmapClientError> {
253 let coroutine = {
254 let session = self.session_or_err()?;
255 let http_auth = &self.inner.http_auth;
256 JmapFlagStore::new(session, http_auth, mailbox, ids, flags, op)?
257 };
258 self.run(coroutine)
259 }
260
261 pub fn get_message(&mut self, mailbox: &str, id: &str) -> Result<Vec<u8>, JmapClientError> {
264 let coroutine = {
265 let session = self.session_or_err()?;
266 let http_auth = &self.inner.http_auth;
267 JmapMessageGet::new(session, http_auth, mailbox, id)?
268 };
269 self.run(coroutine)
270 }
271
272 pub fn add_message(
275 &mut self,
276 mailbox: &str,
277 flags: &[Flag],
278 raw: Vec<u8>,
279 ) -> Result<String, JmapClientError> {
280 let coroutine = {
281 let session = self.session_or_err()?;
282 let http_auth = &self.inner.http_auth;
283 JmapMessageAdd::new(session, http_auth, mailbox, flags, raw)?
284 };
285 self.run(coroutine)
286 }
287
288 pub fn create_mailbox(&mut self, name: &str) -> Result<(), JmapClientError> {
290 let coroutine = {
291 let session = self.session_or_err()?;
292 let http_auth = &self.inner.http_auth;
293 JmapMailboxCreate::new(session, http_auth, name)?
294 };
295 self.run(coroutine)
296 }
297
298 pub fn delete_mailbox(&mut self, name: &str) -> Result<(), JmapClientError> {
301 let coroutine = {
302 let session = self.session_or_err()?;
303 let http_auth = &self.inner.http_auth;
304 JmapMailboxDelete::new(session, http_auth, name)?
305 };
306 self.run(coroutine)
307 }
308
309 pub fn delete_message(&mut self, mailbox: &str, id: &str) -> Result<(), JmapClientError> {
312 let coroutine = {
313 let session = self.session_or_err()?;
314 let http_auth = &self.inner.http_auth;
315 JmapMessageDelete::new(session, http_auth, mailbox, id)?
316 };
317 self.run(coroutine)
318 }
319
320 pub fn copy_messages(
325 &mut self,
326 from: &str,
327 to: &str,
328 ids: &[&str],
329 ) -> Result<(), JmapClientError> {
330 let coroutine = {
331 let session = self.session_or_err()?;
332 let http_auth = &self.inner.http_auth;
333 JmapMessageCopy::new(session, http_auth, from, to, ids)?
334 };
335 self.run(coroutine)
336 }
337
338 pub fn move_messages(
341 &mut self,
342 from: &str,
343 to: &str,
344 ids: &[&str],
345 ) -> Result<(), JmapClientError> {
346 let coroutine = {
347 let session = self.session_or_err()?;
348 let http_auth = &self.inner.http_auth;
349 JmapMessageMove::new(session, http_auth, from, to, ids)?
350 };
351 self.run(coroutine)
352 }
353
354 pub fn send_message(&mut self, raw: Vec<u8>) -> Result<(), JmapClientError> {
358 let coroutine = {
359 let session = self.session_or_err()?;
360 let http_auth = &self.inner.http_auth;
361 let identity_id = self
362 .identity_id
363 .as_deref()
364 .ok_or(JmapClientError::MissingSession)?;
365 let drafts_id = self
366 .drafts_mailbox_id
367 .as_deref()
368 .ok_or(JmapClientError::MissingSession)?;
369 JmapMessageSend::new(session, http_auth, identity_id, drafts_id, raw)?
370 };
371 self.run(coroutine)
372 }
373
374 pub fn watch_mailbox(
382 &mut self,
383 mailbox: &str,
384 shutdown: Arc<AtomicBool>,
385 tx: Sender<WatchEvent>,
386 ) -> Result<(), JmapClientError> {
387 let mut coroutine = {
388 let session = self.session_or_err()?;
389 let http_auth = &self.inner.http_auth;
390 JmapWatchMailbox::new(session, http_auth, mailbox, shutdown)?
391 };
392 let mut buf = [0u8; READ_BUFFER_SIZE];
393 let mut bytes: Option<&[u8]> = None;
394
395 loop {
396 match coroutine.resume(bytes) {
397 JmapCoroutineState::Complete(result) => return Ok(result?),
398 JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsRead) => {
399 match self.inner.stream.read(&mut buf) {
400 Ok(n) => bytes = Some(&buf[..n]),
401 Err(err) if err.kind() == ErrorKind::WouldBlock => bytes = None,
402 Err(err) if err.kind() == ErrorKind::TimedOut => bytes = None,
403 Err(err) => return Err(err.into()),
404 }
405 }
406 JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsWrite(out)) => {
407 self.inner.stream.write_all(&out)?;
408 bytes = None;
409 }
410 JmapCoroutineState::Yielded(JmapWatchMailboxYield::Event(evt)) => {
411 if tx.send(evt).is_err() {
412 return Ok(());
413 }
414 bytes = None;
415 }
416 }
417 }
418 }
419
420 pub fn diff_envelopes(
432 &mut self,
433 _mailbox: &str,
434 state: Option<&[u8]>,
435 ) -> Result<EnvelopeDiff, JmapClientError> {
436 let Some(since_state) = state.and_then(envelope_diff::decode) else {
437 return self.diff_baseline();
438 };
439
440 let mut created_ids: Vec<String> = Vec::new();
441 let mut updated_ids: Vec<String> = Vec::new();
442 let mut destroyed_ids: Vec<String> = Vec::new();
443 let mut cursor = since_state;
444
445 loop {
446 let changes = match self
447 .inner
448 .email_changes(cursor.clone(), JmapEmailChangesOptions::default())
449 {
450 Ok(c) => c,
451 Err(err) if envelope_diff::is_cannot_calculate_changes(&err) => {
452 return self.diff_baseline();
453 }
454 Err(err) => return Err(err.into()),
455 };
456
457 created_ids.extend(changes.created);
458 updated_ids.extend(changes.updated);
459 destroyed_ids.extend(changes.destroyed);
460
461 cursor = changes.new_state;
462 if !changes.has_more_changes {
463 break;
464 }
465 }
466
467 let properties = envelope_properties();
468
469 let new_envelopes = if created_ids.is_empty() {
470 Vec::new()
471 } else {
472 let opts = JmapEmailGetOptions {
473 properties: Some(properties.clone()),
474 ..Default::default()
475 };
476 let output = self.inner.email_get(created_ids, opts)?;
477 output.emails.into_iter().map(envelope_from).collect()
478 };
479
480 let flag_updates = if updated_ids.is_empty() {
481 Vec::new()
482 } else {
483 let opts = JmapEmailGetOptions {
484 properties: Some(properties),
485 ..Default::default()
486 };
487 let output = self.inner.email_get(updated_ids, opts)?;
488 output
489 .emails
490 .into_iter()
491 .map(envelope_from)
492 .map(|env| FlagUpdate {
493 id: env.id,
494 flags: env.flags,
495 })
496 .collect()
497 };
498
499 Ok(EnvelopeDiff::Incremental {
500 new_state: envelope_diff::encode(&cursor),
501 flag_updates,
502 new_envelopes,
503 vanished_ids: destroyed_ids,
504 })
505 }
506
507 pub fn diff_mailboxes(&mut self, state: Option<&[u8]>) -> Result<MailboxDiff, JmapClientError> {
514 let Some(since_state) = state.and_then(envelope_diff::decode) else {
515 let opts = JmapMailboxGetOptions {
516 ids: Some(Vec::new()),
517 ..Default::default()
518 };
519 let output = self.inner.mailbox_get(opts)?;
520 return Ok(MailboxDiff::Changed {
521 new_state: Some(envelope_diff::encode(&output.new_state)),
522 });
523 };
524
525 match self
526 .inner
527 .mailbox_changes(since_state, JmapMailboxChangesOptions::default())
528 {
529 Ok(changes)
530 if !changes.has_more_changes
531 && changes.created.is_empty()
532 && changes.updated.is_empty()
533 && changes.destroyed.is_empty() =>
534 {
535 Ok(MailboxDiff::Unchanged {
536 new_state: envelope_diff::encode(&changes.new_state),
537 })
538 }
539 Ok(changes) => Ok(MailboxDiff::Changed {
540 new_state: Some(envelope_diff::encode(&changes.new_state)),
541 }),
542 Err(InnerJmapClientStdError::MailboxChanges(JmapMailboxChangesError::Changes(
543 JmapChangesError::Method(JmapMethodError::CannotCalculateChanges { .. }),
544 ))) => Ok(MailboxDiff::Changed { new_state: None }),
545 Err(err) => Err(err.into()),
546 }
547 }
548
549 fn diff_baseline(&mut self) -> Result<EnvelopeDiff, JmapClientError> {
553 let output = self
554 .inner
555 .email_get(Vec::new(), JmapEmailGetOptions::default())?;
556 Ok(EnvelopeDiff::FullListRequired {
557 new_state: Some(envelope_diff::encode(&output.new_state)),
558 })
559 }
560
561 fn session_or_err(&self) -> Result<&JmapSession, JmapClientError> {
562 self.inner
563 .session
564 .as_ref()
565 .ok_or(JmapClientError::MissingSession)
566 }
567}
568
569#[cfg(any(
570 feature = "rustls-ring",
571 feature = "rustls-aws",
572 feature = "native-tls"
573))]
574impl JmapClientStd {
575 pub fn connect(url: &Url, tls: &Tls, http_auth: SecretString) -> Result<Self, JmapClientError> {
582 let mut inner = InnerJmapClientStd::connect(url, tls, http_auth)?;
583 inner.session_get(url)?;
584 Ok(Self {
585 inner,
586 identity_id: None,
587 drafts_mailbox_id: None,
588 })
589 }
590}