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//! [](https://crates.io/crates/jmap-client)
15//! [](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
16//! [](https://docs.rs/jmap-client)
17//! [](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}