jmap_client/
lib.rs

1/*
2 * Copyright Stalwart Labs Ltd. See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12//! # jmap-client
13//!
14//! [![crates.io](https://img.shields.io/crates/v/jmap-client)](https://crates.io/crates/jmap-client)
15//! [![build](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
16//! [![docs.rs](https://img.shields.io/docsrs/jmap-client)](https://docs.rs/jmap-client)
17//! [![crates.io](https://img.shields.io/crates/l/jmap-client)](http://www.apache.org/licenses/LICENSE-2.0)
18//!
19//! _jmap-client_ is a **JSON Meta Application Protocol (JMAP) library** written in Rust. The library is a full implementation of the JMAP RFCs including:
20//!
21//! - JMAP Core ([RFC 8620](https://datatracker.ietf.org/doc/html/rfc8620))
22//! - JMAP for Mail ([RFC 8621](https://datatracker.ietf.org/doc/html/rfc8621))
23//! - JMAP over WebSocket ([RFC 8887](https://datatracker.ietf.org/doc/html/rfc8887)).
24//! - JMAP for Sieve Scripts ([DRAFT-SIEVE-12](https://www.ietf.org/archive/id/draft-ietf-jmap-sieve-12.html)).
25//!
26//! Features:
27//!
28//! - Async and blocking support (use the cargo feature ``blocking`` to enable blocking).
29//! - WebSocket async streams (use the cargo feature ``websockets`` to enable JMAP over WebSocket).
30//! - EventSource async streams.
31//! - Helper functions to reduce boilerplate code and quickly build JMAP requests.
32//! - Fast parsing and encoding of JMAP requests.
33//!
34//! ## Usage Example
35//!
36//! ```rust
37//!     // Connect to the JMAP server using Basic authentication.
38//!     // (just for demonstration purposes, Bearer tokens should be used instead)
39//!     let client = Client::new()
40//!         .credentials(("john@example.org", "secret"))
41//!         .connect("https://jmap.example.org")
42//!         .await
43//!         .unwrap();
44//!
45//!     // Create a mailbox.
46//!     let mailbox_id = client
47//!         .mailbox_create("My Mailbox", None::<String>, Role::None)
48//!         .await
49//!         .unwrap()
50//!         .take_id();
51//!
52//!     // Import a message into the mailbox.
53//!     client
54//!         .email_import(
55//!             b"From: john@example.org\nSubject: test\n\n test".to_vec(),
56//!             [&mailbox_id],
57//!             ["$draft"].into(),
58//!             None,
59//!         )
60//!         .await
61//!         .unwrap();
62//!
63//!     // Obtain all e-mail ids matching a filter.
64//!     let email_id = client
65//!         .email_query(
66//!             Filter::and([
67//!                 email::query::Filter::subject("test"),
68//!                 email::query::Filter::in_mailbox(&mailbox_id),
69//!                 email::query::Filter::has_keyword("$draft"),
70//!             ])
71//!             .into(),
72//!             [email::query::Comparator::from()].into(),
73//!         )
74//!         .await
75//!         .unwrap()
76//!         .take_ids()
77//!         .pop()
78//!         .unwrap();
79//!
80//!     // Fetch an e-mail message.
81//!     let email = client
82//!         .email_get(
83//!             &email_id,
84//!             [Property::Subject, Property::Preview, Property::Keywords].into(),
85//!         )
86//!         .await
87//!         .unwrap()
88//!         .unwrap();
89//!     assert_eq!(email.preview().unwrap(), "test");
90//!     assert_eq!(email.subject().unwrap(), "test");
91//!     assert_eq!(email.keywords(), ["$draft"]);
92//!
93//!     // Fetch only the updated properties of all mailboxes that changed
94//!     // since a state.
95//!     let mut request = client.build();
96//!     let changes_request = request.changes_mailbox("n").max_changes(0);
97//!     let properties_ref = changes_request.updated_properties_reference();
98//!     let updated_ref = changes_request.updated_reference();
99//!     request
100//!         .get_mailbox()
101//!         .ids_ref(updated_ref)
102//!         .properties_ref(properties_ref);
103//!     for mailbox in request
104//!         .send()
105//!         .await
106//!         .unwrap()
107//!         .unwrap_method_responses()
108//!         .pop()
109//!         .unwrap()
110//!         .unwrap_get_mailbox()
111//!         .unwrap()
112//!         .take_list()
113//!     {
114//!         println!("Changed mailbox: {:#?}", mailbox);
115//!     }
116//!
117//!     // Delete the mailbox including any messages
118//!     client.mailbox_destroy(&mailbox_id, true).await.unwrap();
119//!
120//!     // Open an EventSource connection with the JMAP server.
121//!     let mut stream = client
122//!         .event_source(
123//!             [
124//!                 TypeState::Email,
125//!                 TypeState::EmailDelivery,
126//!                 TypeState::Mailbox,
127//!                 TypeState::EmailSubmission,
128//!                 TypeState::Identity,
129//!             ]
130//!             .into(),
131//!             false,
132//!             60.into(),
133//!             None,
134//!         )
135//!         .await
136//!         .unwrap();
137//!
138//!     // Consume events received over EventSource.
139//!     while let Some(event) = stream.next().await {
140//!         let changes = event.unwrap();
141//!         println!("-> Change id: {:?}", changes.id());
142//!         for account_id in changes.changed_accounts() {
143//!             println!(" Account {} has changes:", account_id);
144//!             if let Some(account_changes) = changes.changes(account_id) {
145//!                 for (type_state, state_id) in account_changes {
146//!                     println!("   Type {:?} has a new state {}.", type_state, state_id);
147//!                 }
148//!             }
149//!         }
150//!     }
151//! ```
152//!
153//! More examples available under the [examples](examples) directory.
154//!
155//! ## Testing
156//!
157//! To run the testsuite:
158//!
159//! ```bash
160//!  $ cargo test --all-features
161//! ```
162//!
163//! ## Conformed RFCs
164//!
165//! - [RFC 8620 - The JSON Meta Application Protocol (JMAP)](https://datatracker.ietf.org/doc/html/rfc8620)
166//! - [RFC 8621 - The JSON Meta Application Protocol (JMAP) for Mail](https://datatracker.ietf.org/doc/html/rfc8621)
167//! - [RFC 8887 - A JSON Meta Application Protocol (JMAP) Subprotocol for WebSocket](https://datatracker.ietf.org/doc/html/rfc8887)
168//!
169//! ## License
170//!
171//! Licensed under either of
172//!
173//!  * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
174//!  * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
175//!
176//! at your option.
177//!
178//! ## Copyright
179//!
180//! Copyright (C) 2022, Stalwart Labs Ltd.
181//!
182
183#[forbid(unsafe_code)]
184pub mod blob;
185pub mod client;
186pub mod core;
187pub mod email;
188pub mod email_submission;
189#[cfg(feature = "async")]
190pub mod event_source;
191pub mod identity;
192pub mod mailbox;
193pub mod principal;
194pub mod push_subscription;
195pub mod sieve;
196pub mod thread;
197pub mod vacation_response;
198
199use crate::core::error::MethodError;
200use crate::core::error::ProblemDetails;
201use crate::core::set::SetError;
202use ahash::AHashMap;
203use serde::{Deserialize, Serialize};
204use std::fmt::Display;
205
206#[cfg(feature = "websockets")]
207pub mod client_ws;
208
209#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
210pub enum URI {
211    #[serde(rename = "urn:ietf:params:jmap:core")]
212    Core,
213    #[serde(rename = "urn:ietf:params:jmap:mail")]
214    Mail,
215    #[serde(rename = "urn:ietf:params:jmap:submission")]
216    Submission,
217    #[serde(rename = "urn:ietf:params:jmap:vacationresponse")]
218    VacationResponse,
219    #[serde(rename = "urn:ietf:params:jmap:contacts")]
220    Contacts,
221    #[serde(rename = "urn:ietf:params:jmap:calendars")]
222    Calendars,
223    #[serde(rename = "urn:ietf:params:jmap:websocket")]
224    WebSocket,
225    #[serde(rename = "urn:ietf:params:jmap:sieve")]
226    Sieve,
227    #[serde(rename = "urn:ietf:params:jmap:principals")]
228    Principals,
229    #[serde(rename = "urn:ietf:params:jmap:principals:owner")]
230    PrincipalsOwner,
231}
232
233impl AsRef<str> for URI {
234    fn as_ref(&self) -> &str {
235        match self {
236            URI::Core => "urn:ietf:params:jmap:core",
237            URI::Mail => "urn:ietf:params:jmap:mail",
238            URI::Submission => "urn:ietf:params:jmap:submission",
239            URI::VacationResponse => "urn:ietf:params:jmap:vacationresponse",
240            URI::Contacts => "urn:ietf:params:jmap:contacts",
241            URI::Calendars => "urn:ietf:params:jmap:calendars",
242            URI::WebSocket => "urn:ietf:params:jmap:websocket",
243            URI::Sieve => "urn:ietf:params:jmap:sieve",
244            URI::Principals => "urn:ietf:params:jmap:principals",
245            URI::PrincipalsOwner => "urn:ietf:params:jmap:principals:owner",
246        }
247    }
248}
249
250#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
251pub enum Method {
252    #[serde(rename = "Core/echo")]
253    Echo,
254    #[serde(rename = "Blob/copy")]
255    CopyBlob,
256    #[serde(rename = "PushSubscription/get")]
257    GetPushSubscription,
258    #[serde(rename = "PushSubscription/set")]
259    SetPushSubscription,
260    #[serde(rename = "Mailbox/get")]
261    GetMailbox,
262    #[serde(rename = "Mailbox/changes")]
263    ChangesMailbox,
264    #[serde(rename = "Mailbox/query")]
265    QueryMailbox,
266    #[serde(rename = "Mailbox/queryChanges")]
267    QueryChangesMailbox,
268    #[serde(rename = "Mailbox/set")]
269    SetMailbox,
270    #[serde(rename = "Thread/get")]
271    GetThread,
272    #[serde(rename = "Thread/changes")]
273    ChangesThread,
274    #[serde(rename = "Email/get")]
275    GetEmail,
276    #[serde(rename = "Email/changes")]
277    ChangesEmail,
278    #[serde(rename = "Email/query")]
279    QueryEmail,
280    #[serde(rename = "Email/queryChanges")]
281    QueryChangesEmail,
282    #[serde(rename = "Email/set")]
283    SetEmail,
284    #[serde(rename = "Email/copy")]
285    CopyEmail,
286    #[serde(rename = "Email/import")]
287    ImportEmail,
288    #[serde(rename = "Email/parse")]
289    ParseEmail,
290    #[serde(rename = "SearchSnippet/get")]
291    GetSearchSnippet,
292    #[serde(rename = "Identity/get")]
293    GetIdentity,
294    #[serde(rename = "Identity/changes")]
295    ChangesIdentity,
296    #[serde(rename = "Identity/set")]
297    SetIdentity,
298    #[serde(rename = "EmailSubmission/get")]
299    GetEmailSubmission,
300    #[serde(rename = "EmailSubmission/changes")]
301    ChangesEmailSubmission,
302    #[serde(rename = "EmailSubmission/query")]
303    QueryEmailSubmission,
304    #[serde(rename = "EmailSubmission/queryChanges")]
305    QueryChangesEmailSubmission,
306    #[serde(rename = "EmailSubmission/set")]
307    SetEmailSubmission,
308    #[serde(rename = "VacationResponse/get")]
309    GetVacationResponse,
310    #[serde(rename = "VacationResponse/set")]
311    SetVacationResponse,
312    #[serde(rename = "SieveScript/get")]
313    GetSieveScript,
314    #[serde(rename = "SieveScript/set")]
315    SetSieveScript,
316    #[serde(rename = "SieveScript/query")]
317    QuerySieveScript,
318    #[serde(rename = "SieveScript/validate")]
319    ValidateSieveScript,
320    #[serde(rename = "Principal/get")]
321    GetPrincipal,
322    #[serde(rename = "Principal/changes")]
323    ChangesPrincipal,
324    #[serde(rename = "Principal/query")]
325    QueryPrincipal,
326    #[serde(rename = "Principal/queryChanges")]
327    QueryChangesPrincipal,
328    #[serde(rename = "Principal/set")]
329    SetPrincipal,
330    #[serde(rename = "error")]
331    Error,
332}
333
334#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)]
335pub enum TypeState {
336    Mailbox,
337    Thread,
338    Email,
339    EmailDelivery,
340    Identity,
341    EmailSubmission,
342}
343
344#[derive(Debug, Serialize, Deserialize)]
345pub enum StateChangeType {
346    StateChange,
347}
348
349#[derive(Debug, Deserialize)]
350pub struct StateChange {
351    #[serde(rename = "@type")]
352    pub type_: StateChangeType,
353    pub changed: AHashMap<String, AHashMap<TypeState, String>>,
354}
355
356#[derive(Debug, Clone)]
357pub struct Get;
358#[derive(Debug, Clone)]
359pub struct Set;
360
361pub type Result<T> = std::result::Result<T, Error>;
362
363#[derive(Debug)]
364pub enum Error {
365    Transport(reqwest::Error),
366    Parse(serde_json::Error),
367    Internal(String),
368    Problem(Box<ProblemDetails>),
369    Server(String),
370    Method(MethodError),
371    Set(SetError<String>),
372    #[cfg(feature = "websockets")]
373    WebSocket(tokio_tungstenite::tungstenite::error::Error),
374}
375
376impl std::error::Error for Error {}
377
378impl From<reqwest::Error> for Error {
379    fn from(e: reqwest::Error) -> Self {
380        Error::Transport(e)
381    }
382}
383
384impl From<serde_json::Error> for Error {
385    fn from(e: serde_json::Error) -> Self {
386        Error::Parse(e)
387    }
388}
389
390impl From<MethodError> for Error {
391    fn from(e: MethodError) -> Self {
392        Error::Method(e)
393    }
394}
395
396impl From<ProblemDetails> for Error {
397    fn from(e: ProblemDetails) -> Self {
398        Error::Problem(Box::new(e))
399    }
400}
401
402impl From<SetError<String>> for Error {
403    fn from(e: SetError<String>) -> Self {
404        Error::Set(e)
405    }
406}
407
408impl From<&str> for Error {
409    fn from(s: &str) -> Self {
410        Error::Internal(s.to_string())
411    }
412}
413
414#[cfg(feature = "websockets")]
415impl From<tokio_tungstenite::tungstenite::error::Error> for Error {
416    fn from(e: tokio_tungstenite::tungstenite::error::Error) -> Self {
417        Error::WebSocket(e)
418    }
419}
420
421impl Display for Error {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        match self {
424            Error::Transport(e) => write!(f, "Transport error: {}", e),
425            Error::Parse(e) => write!(f, "Parse error: {}", e),
426            Error::Internal(e) => write!(f, "Internal error: {}", e),
427            Error::Problem(e) => write!(f, "Request failed: {}", e),
428            Error::Server(e) => write!(f, "Server failed: {}", e),
429            Error::Method(e) => write!(f, "Request failed: {}", e),
430            Error::Set(e) => write!(f, "Set failed: {}", e),
431            #[cfg(feature = "websockets")]
432            Error::WebSocket(e) => write!(f, "WebSockets error: {}", e),
433        }
434    }
435}
436
437impl Display for TypeState {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        match self {
440            TypeState::Mailbox => write!(f, "Mailbox"),
441            TypeState::Thread => write!(f, "Thread"),
442            TypeState::Email => write!(f, "Email"),
443            TypeState::EmailDelivery => write!(f, "EmailDelivery"),
444            TypeState::Identity => write!(f, "Identity"),
445            TypeState::EmailSubmission => write!(f, "EmailSubmission"),
446        }
447    }
448}