jmap_client/
lib.rs

1/*
2 * Copyright Stalwart Labs LLC 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//!                 DataType::Email,
125//!                 DataType::EmailDelivery,
126//!                 DataType::Mailbox,
127//!                 DataType::EmailSubmission,
128//!                 DataType::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 LLC
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 DataType {
336    #[serde(rename = "Email")]
337    Email = 0,
338    #[serde(rename = "EmailDelivery")]
339    EmailDelivery = 1,
340    #[serde(rename = "EmailSubmission")]
341    EmailSubmission = 2,
342    #[serde(rename = "Mailbox")]
343    Mailbox = 3,
344    #[serde(rename = "Thread")]
345    Thread = 4,
346    #[serde(rename = "Identity")]
347    Identity = 5,
348    #[serde(rename = "Core")]
349    Core = 6,
350    #[serde(rename = "PushSubscription")]
351    PushSubscription = 7,
352    #[serde(rename = "SearchSnippet")]
353    SearchSnippet = 8,
354    #[serde(rename = "VacationResponse")]
355    VacationResponse = 9,
356    #[serde(rename = "MDN")]
357    Mdn = 10,
358    #[serde(rename = "Quota")]
359    Quota = 11,
360    #[serde(rename = "SieveScript")]
361    SieveScript = 12,
362    #[serde(rename = "Calendar")]
363    Calendar = 13,
364    #[serde(rename = "CalendarEvent")]
365    CalendarEvent = 14,
366    #[serde(rename = "CalendarEventNotification")]
367    CalendarEventNotification = 15,
368    #[serde(rename = "AddressBook")]
369    AddressBook = 16,
370    #[serde(rename = "ContactCard")]
371    ContactCard = 17,
372    #[serde(rename = "FileNode")]
373    FileNode = 18,
374    #[serde(rename = "Principal")]
375    Principal = 19,
376    #[serde(rename = "ShareNotification")]
377    ShareNotification = 20,
378    #[serde(rename = "ParticipantIdentity")]
379    ParticipantIdentity = 21,
380    #[serde(rename = "CalendarAlert")]
381    CalendarAlert = 22,
382}
383
384#[derive(Serialize, Deserialize, Debug)]
385#[serde(tag = "@type")]
386pub enum PushObject {
387    StateChange {
388        changed: AHashMap<String, AHashMap<DataType, String>>,
389    },
390    EmailPush {
391        #[serde(rename = "accountId")]
392        account_id: String,
393        email: serde_json::Value,
394    },
395    CalendarAlert(CalendarAlert),
396    Group {
397        entries: Vec<PushObject>,
398    },
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct CalendarAlert {
403    #[serde(rename = "accountId")]
404    pub account_id: String,
405    #[serde(rename = "calendarEventId")]
406    pub calendar_event_id: String,
407    pub uid: String,
408    #[serde(rename = "recurrenceId")]
409    pub recurrence_id: Option<String>,
410    #[serde(rename = "alertId")]
411    pub alert_id: String,
412}
413
414#[derive(Debug, Clone)]
415pub struct Get;
416#[derive(Debug, Clone)]
417pub struct Set;
418
419pub type Result<T> = std::result::Result<T, Error>;
420
421#[derive(Debug)]
422pub enum Error {
423    Transport(reqwest::Error),
424    Parse(serde_json::Error),
425    Internal(String),
426    Problem(Box<ProblemDetails>),
427    Server(String),
428    Method(MethodError),
429    Set(SetError<String>),
430    #[cfg(feature = "websockets")]
431    WebSocket(tokio_tungstenite::tungstenite::error::Error),
432}
433
434impl std::error::Error for Error {}
435
436impl From<reqwest::Error> for Error {
437    fn from(e: reqwest::Error) -> Self {
438        Error::Transport(e)
439    }
440}
441
442impl From<serde_json::Error> for Error {
443    fn from(e: serde_json::Error) -> Self {
444        Error::Parse(e)
445    }
446}
447
448impl From<MethodError> for Error {
449    fn from(e: MethodError) -> Self {
450        Error::Method(e)
451    }
452}
453
454impl From<ProblemDetails> for Error {
455    fn from(e: ProblemDetails) -> Self {
456        Error::Problem(Box::new(e))
457    }
458}
459
460impl From<SetError<String>> for Error {
461    fn from(e: SetError<String>) -> Self {
462        Error::Set(e)
463    }
464}
465
466impl From<&str> for Error {
467    fn from(s: &str) -> Self {
468        Error::Internal(s.to_string())
469    }
470}
471
472#[cfg(feature = "websockets")]
473impl From<tokio_tungstenite::tungstenite::error::Error> for Error {
474    fn from(e: tokio_tungstenite::tungstenite::error::Error) -> Self {
475        Error::WebSocket(e)
476    }
477}
478
479impl Display for Error {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        match self {
482            Error::Transport(e) => write!(f, "Transport error: {}", e),
483            Error::Parse(e) => write!(f, "Parse error: {}", e),
484            Error::Internal(e) => write!(f, "Internal error: {}", e),
485            Error::Problem(e) => write!(f, "Request failed: {}", e),
486            Error::Server(e) => write!(f, "Server failed: {}", e),
487            Error::Method(e) => write!(f, "Request failed: {}", e),
488            Error::Set(e) => write!(f, "Set failed: {}", e),
489            #[cfg(feature = "websockets")]
490            Error::WebSocket(e) => write!(f, "WebSockets error: {}", e),
491        }
492    }
493}
494
495impl Display for DataType {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        match self {
498            DataType::Mailbox => write!(f, "Mailbox"),
499            DataType::Thread => write!(f, "Thread"),
500            DataType::Email => write!(f, "Email"),
501            DataType::EmailDelivery => write!(f, "EmailDelivery"),
502            DataType::Identity => write!(f, "Identity"),
503            DataType::EmailSubmission => write!(f, "EmailSubmission"),
504            DataType::CalendarAlert => write!(f, "CalendarAlert"),
505            DataType::Core => write!(f, "Core"),
506            DataType::PushSubscription => write!(f, "PushSubscription"),
507            DataType::SearchSnippet => write!(f, "SearchSnippet"),
508            DataType::VacationResponse => write!(f, "VacationResponse"),
509            DataType::Mdn => write!(f, "MDN"),
510            DataType::Quota => write!(f, "Quota"),
511            DataType::SieveScript => write!(f, "SieveScript"),
512            DataType::Calendar => write!(f, "Calendar"),
513            DataType::CalendarEvent => write!(f, "CalendarEvent"),
514            DataType::CalendarEventNotification => write!(f, "CalendarEventNotification"),
515            DataType::AddressBook => write!(f, "AddressBook"),
516            DataType::ContactCard => write!(f, "ContactCard"),
517            DataType::FileNode => write!(f, "FileNode"),
518            DataType::Principal => write!(f, "Principal"),
519            DataType::ShareNotification => write!(f, "ShareNotification"),
520            DataType::ParticipantIdentity => write!(f, "ParticipantIdentity"),
521        }
522    }
523}