main
Mauro D 2022-09-09 09:49:32 +00:00
parent 5e9c4f825e
commit 2e70d350ed
8 changed files with 786 additions and 9 deletions

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
jmap-client 0.1.0
================================
- First release.

171
README.md
View File

@ -1,4 +1,169 @@
JMAP Client for Rust # jmap-client
====
[![crates.io](https://img.shields.io/crates/v/jmap-client)](https://crates.io/crates/jmap-client)
[![build](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/jmap-client)](https://docs.rs/jmap-client)
[![crates.io](https://img.shields.io/crates/l/jmap-client)](http://www.apache.org/licenses/LICENSE-2.0)
_jmap-client_ is a **JSON Meta Application Protocol (JMAP) library** written in Rust. The library is a full implementation of the JMAP RFCs including:
- JMAP Core ([RFC 8620](https://datatracker.ietf.org/doc/html/rfc8620))
- JMAP for Mail ([RFC 8621](https://datatracker.ietf.org/doc/html/rfc8621))
- JMAP over WebSocket ([RFC 8887](https://datatracker.ietf.org/doc/html/rfc8887)).
Features:
- Async and blocking support (use the cargo feature ``blocking`` to enable blocking).
- WebSocket async streams (use the cargo feature ``websockets`` to enable JMAP over WebSocket).
- EventSource async streams.
- Helper functions to reduce boilerplate code and quickly build JMAP requests.
- Fast parsing and encoding of JMAP requests.
## Usage Example
```rust
// Connect to the JMAP server using Basic authentication.
// (just for demonstration purposes, Bearer tokens should be used instead)
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Create a mailbox.
let mailbox_id = client
.mailbox_create("My Mailbox", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Import a message into the mailbox.
client
.email_import(
b"From: john@example.org\nSubject: test\n\n test".to_vec(),
[&mailbox_id],
["$draft"].into(),
None,
)
.await
.unwrap();
// Obtain all e-mail ids matching a filter.
let email_id = client
.email_query(
Filter::and([
email::query::Filter::subject("test"),
email::query::Filter::in_mailbox(&mailbox_id),
email::query::Filter::has_keyword("$draft"),
])
.into(),
[email::query::Comparator::from()].into(),
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap();
// Fetch an e-mail message.
let email = client
.email_get(
&email_id,
[Property::Subject, Property::Preview, Property::Keywords].into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(email.preview().unwrap(), "test");
assert_eq!(email.subject().unwrap(), "test");
assert_eq!(email.keywords(), ["$draft"]);
// Fetch only the updated properties of all mailboxes that changed
// since a state.
let mut request = client.build();
let changes_request = request.changes_mailbox("n").max_changes(0);
let properties_ref = changes_request.updated_properties_reference();
let updated_ref = changes_request.updated_reference();
request
.get_mailbox()
.ids_ref(updated_ref)
.properties_ref(properties_ref);
for mailbox in request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap()
.take_list()
{
println!("Changed mailbox: {:#?}", mailbox);
}
// Delete the mailbox including any messages
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
// Open an EventSource connection with the JMAP server.
let mut stream = client
.event_source(
[
TypeState::Email,
TypeState::EmailDelivery,
TypeState::Mailbox,
TypeState::EmailSubmission,
TypeState::Identity,
]
.into(),
false,
60.into(),
None,
)
.await
.unwrap();
// Consume events received over EventSource.
while let Some(event) = stream.next().await {
let changes = event.unwrap();
println!("-> Change id: {:?}", changes.id());
for account_id in changes.changed_accounts() {
println!(" Account {} has changes:", account_id);
if let Some(account_changes) = changes.changes(account_id) {
for (type_state, state_id) in account_changes {
println!(" Type {:?} has a new state {}.", type_state, state_id);
}
}
}
}
```
More examples available under the [examples](examples) directory.
## Testing
To run the testsuite:
```bash
$ cargo test --all-features
```
## Conformed RFCs
- [RFC 8620 - The JSON Meta Application Protocol (JMAP)](https://datatracker.ietf.org/doc/html/rfc8620)
- [RFC 8621 - The JSON Meta Application Protocol (JMAP) for Mail](https://datatracker.ietf.org/doc/html/rfc8621)
- [RFC 8887 - A JSON Meta Application Protocol (JMAP) Subprotocol for WebSocket](https://datatracker.ietf.org/doc/html/rfc8887)
## License
Licensed under either of
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Copyright
Copyright (C) 2022, Stalwart Labs Ltd.
(documentation is a work in progress)

62
examples/eventsource.rs Normal file
View File

@ -0,0 +1,62 @@
/*
* Copyright Stalwart Labs Ltd. See the COPYING
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
* option. This file may not be copied, modified, or distributed
* except according to those terms.
*/
use futures_util::StreamExt;
use jmap_client::{client::Client, TypeState};
async fn event_source() {
// Connect to the JMAP server using Basic authentication
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Open EventSource connection
let mut stream = client
.event_source(
[
TypeState::Email,
TypeState::EmailDelivery,
TypeState::Mailbox,
TypeState::EmailSubmission,
TypeState::Identity,
]
.into(),
false,
60.into(),
None,
)
.await
.unwrap();
// Consume events
while let Some(event) = stream.next().await {
let changes = event.unwrap();
println!("-> Change id: {:?}", changes.id());
for account_id in changes.changed_accounts() {
println!(" Account {} has changes:", account_id);
if let Some(account_changes) = changes.changes(account_id) {
for (type_state, state_id) in account_changes {
println!(" Type {:?} has a new state {}.", type_state, state_id);
}
}
}
}
}
fn main() {
let _c = event_source();
}

69
examples/mailboxes.rs Normal file
View File

@ -0,0 +1,69 @@
/*
* Copyright Stalwart Labs Ltd. See the COPYING
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
* option. This file may not be copied, modified, or distributed
* except according to those terms.
*/
use jmap_client::{
client::Client,
mailbox::{query::Filter, Role},
};
async fn mailboxes() {
// Connect to the JMAP server using Basic authentication
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Create a mailbox
let mailbox_id = client
.mailbox_create("My Mailbox", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Rename a mailbox
client
.mailbox_rename(&mailbox_id, "My Renamed Mailbox")
.await
.unwrap();
// Query mailboxes to obtain Inbox's id
let inbox_id = client
.mailbox_query(Filter::role(Role::Inbox).into(), None::<Vec<_>>)
.await
.unwrap()
.take_ids()
.pop()
.unwrap();
// Print Inbox's details
println!(
"{:?}",
client.mailbox_get(&inbox_id, None::<Vec<_>>).await.unwrap()
);
// Move the newly created mailbox under Inbox
client
.mailbox_move(&mailbox_id, inbox_id.into())
.await
.unwrap();
// Delete the mailbox including any messages
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
}
fn main() {
let _c = mailboxes();
}

120
examples/messages.rs Normal file
View File

@ -0,0 +1,120 @@
/*
* Copyright Stalwart Labs Ltd. See the COPYING
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
* option. This file may not be copied, modified, or distributed
* except according to those terms.
*/
use jmap_client::{
client::Client,
core::query::Filter,
email::{self, Property},
mailbox::{self, Role},
};
const TEST_MESSAGE: &[u8; 90] = br#"From: john@example.org
To: jane@example.org
Subject: Testing JMAP client
This is a test.
"#;
async fn messages() {
// Connect to the JMAP server using Basic authentication
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Query mailboxes to obtain Inbox and Trash folder id
let inbox_id = client
.mailbox_query(
mailbox::query::Filter::role(Role::Inbox).into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap();
let trash_id = client
.mailbox_query(
mailbox::query::Filter::role(Role::Trash).into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap();
// Import message into inbox
client
.email_import(TEST_MESSAGE.to_vec(), [&inbox_id], ["$draft"].into(), None)
.await
.unwrap();
// Query mailbox
let email_id = client
.email_query(
Filter::and([
email::query::Filter::subject("test"),
email::query::Filter::in_mailbox(&inbox_id),
email::query::Filter::has_keyword("$draft"),
])
.into(),
[email::query::Comparator::from()].into(),
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap();
// Fetch message
let email = client
.email_get(
&email_id,
[Property::Subject, Property::Preview, Property::Keywords].into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(email.preview().unwrap(), "This is a test.");
assert_eq!(email.subject().unwrap(), "Testing JMAP client");
assert_eq!(email.keywords(), ["$draft"]);
// Remove the $draft keyword
client
.email_set_keyword(&email_id, "$draft", false)
.await
.unwrap();
// Replace all keywords
client
.email_set_keywords(&email_id, ["$seen", "$important"])
.await
.unwrap();
// Move the message to the Trash folder
client
.email_set_mailboxes(&email_id, [&trash_id])
.await
.unwrap();
// Destroy the e-mail
client.email_destroy(&email_id).await.unwrap();
}
fn main() {
let _c = messages();
}

View File

@ -0,0 +1,101 @@
/*
* Copyright Stalwart Labs Ltd. See the COPYING
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
* option. This file may not be copied, modified, or distributed
* except according to those terms.
*/
use jmap_client::{client::Client, core::query, email, mailbox};
async fn result_reference() {
// Connect to the JMAP server using Basic authentication
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Delete e-mails matching a filter
let mut request = client.build();
let result_ref = request
.query_email()
.filter(query::Filter::and([
email::query::Filter::has_keyword("$draft"),
email::query::Filter::from("bill"),
]))
.result_reference();
request.set_email().destroy_ref(result_ref);
let _destroyed_ids = request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_set_email()
.unwrap()
.take_destroyed_ids();
// Fetch mailboxes matching a filter
let mut request = client.build();
let query_result = request
.query_mailbox()
.filter(query::Filter::and([
mailbox::query::Filter::has_any_role(false),
mailbox::query::Filter::is_subscribed(true),
]))
.result_reference();
request.get_mailbox().ids_ref(query_result).properties([
mailbox::Property::Id,
mailbox::Property::Name,
mailbox::Property::ParentId,
mailbox::Property::TotalEmails,
mailbox::Property::UnreadEmails,
]);
let _mailboxes = request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap()
.take_list();
// Fetch only the updated properties of all mailboxes that changed
// since a state.
let mut request = client.build();
let changes_request = request.changes_mailbox("n").max_changes(0);
let properties_ref = changes_request.updated_properties_reference();
let updated_ref = changes_request.updated_reference();
request
.get_mailbox()
.ids_ref(updated_ref)
.properties_ref(properties_ref);
for mailbox in request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap()
.take_list()
{
println!("Changed mailbox: {:#?}", mailbox);
}
}
fn main() {
let _c = result_reference();
}

86
examples/websocket.rs Normal file
View File

@ -0,0 +1,86 @@
/*
* Copyright Stalwart Labs Ltd. See the COPYING
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* file at the top-level directory of this distribution.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
* option. This file may not be copied, modified, or distributed
* except according to those terms.
*/
use futures_util::StreamExt;
use jmap_client::{client::Client, client_ws::WebSocketMessage, core::set::SetObject};
use tokio::sync::mpsc;
// Make sure the "websockets" feature is enabled!
async fn websocket() {
// Connect to the JMAP server using Basic authentication
let client = Client::new()
.credentials(("john@example.org", "secret"))
.connect("https://jmap.example.org")
.await
.unwrap();
// Connect to the WebSocket endpoint
let mut ws_stream = client.connect_ws().await.unwrap();
// Read WS messages on a separate thread
let (stream_tx, mut stream_rx) = mpsc::channel::<WebSocketMessage>(100);
tokio::spawn(async move {
while let Some(change) = ws_stream.next().await {
stream_tx.send(change.unwrap()).await.unwrap();
}
});
// Create a mailbox over WS
let mut request = client.build();
let create_id = request
.set_mailbox()
.create()
.name("WebSocket Test")
.create_id()
.unwrap();
let request_id = request.send_ws().await.unwrap();
// Read response from WS stream
let mailbox_id = if let Some(WebSocketMessage::Response(mut response)) = stream_rx.recv().await
{
assert_eq!(request_id, response.request_id().unwrap());
response
.pop_method_response()
.unwrap()
.unwrap_set_mailbox()
.unwrap()
.created(&create_id)
.unwrap()
.take_id()
} else {
unreachable!()
};
// Enable push notifications over WS
client
.enable_push_ws(None::<Vec<_>>, None::<&str>)
.await
.unwrap();
// Make changes over standard HTTP and expect a push notification via WS
client
.mailbox_update_sort_order(&mailbox_id, 1)
.await
.unwrap();
if let Some(WebSocketMessage::StateChange(changes)) = stream_rx.recv().await {
println!("Received changes: {:?}", changes);
} else {
unreachable!()
}
}
fn main() {
let _c = websocket();
}

View File

@ -9,13 +9,177 @@
* except according to those terms. * except according to those terms.
*/ */
use crate::core::error::MethodError; //! # jmap-client
use crate::core::error::ProblemDetails; //!
use crate::core::set::SetError; //! [![crates.io](https://img.shields.io/crates/v/jmap-client)](https://crates.io/crates/jmap-client)
use ahash::AHashMap; //! [![build](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
use serde::{Deserialize, Serialize}; //! [![docs.rs](https://img.shields.io/docsrs/jmap-client)](https://docs.rs/jmap-client)
use std::fmt::Display; //! [![crates.io](https://img.shields.io/crates/l/jmap-client)](http://www.apache.org/licenses/LICENSE-2.0)
//!
//! _jmap-client_ is a **JSON Meta Application Protocol (JMAP) library** written in Rust. The library is a full implementation of the JMAP RFCs including:
//!
//! - JMAP Core ([RFC 8620](https://datatracker.ietf.org/doc/html/rfc8620))
//! - JMAP for Mail ([RFC 8621](https://datatracker.ietf.org/doc/html/rfc8621))
//! - JMAP over WebSocket ([RFC 8887](https://datatracker.ietf.org/doc/html/rfc8887)).
//!
//! Features:
//!
//! - Async and blocking support (use the cargo feature ``blocking`` to enable blocking).
//! - WebSocket async streams (use the cargo feature ``websockets`` to enable JMAP over WebSocket).
//! - EventSource async streams.
//! - Helper functions to reduce boilerplate code and quickly build JMAP requests.
//! - Fast parsing and encoding of JMAP requests.
//!
//! ## Usage Example
//!
//! ```rust
//! // Connect to the JMAP server using Basic authentication.
//! // (just for demonstration purposes, Bearer tokens should be used instead)
//! let client = Client::new()
//! .credentials(("john@example.org", "secret"))
//! .connect("https://jmap.example.org")
//! .await
//! .unwrap();
//!
//! // Create a mailbox.
//! let mailbox_id = client
//! .mailbox_create("My Mailbox", None::<String>, Role::None)
//! .await
//! .unwrap()
//! .take_id();
//!
//! // Import a message into the mailbox.
//! client
//! .email_import(
//! b"From: john@example.org\nSubject: test\n\n test".to_vec(),
//! [&mailbox_id],
//! ["$draft"].into(),
//! None,
//! )
//! .await
//! .unwrap();
//!
//! // Obtain all e-mail ids matching a filter.
//! let email_id = client
//! .email_query(
//! Filter::and([
//! email::query::Filter::subject("test"),
//! email::query::Filter::in_mailbox(&mailbox_id),
//! email::query::Filter::has_keyword("$draft"),
//! ])
//! .into(),
//! [email::query::Comparator::from()].into(),
//! )
//! .await
//! .unwrap()
//! .take_ids()
//! .pop()
//! .unwrap();
//!
//! // Fetch an e-mail message.
//! let email = client
//! .email_get(
//! &email_id,
//! [Property::Subject, Property::Preview, Property::Keywords].into(),
//! )
//! .await
//! .unwrap()
//! .unwrap();
//! assert_eq!(email.preview().unwrap(), "test");
//! assert_eq!(email.subject().unwrap(), "test");
//! assert_eq!(email.keywords(), ["$draft"]);
//!
//! // Fetch only the updated properties of all mailboxes that changed
//! // since a state.
//! let mut request = client.build();
//! let changes_request = request.changes_mailbox("n").max_changes(0);
//! let properties_ref = changes_request.updated_properties_reference();
//! let updated_ref = changes_request.updated_reference();
//! request
//! .get_mailbox()
//! .ids_ref(updated_ref)
//! .properties_ref(properties_ref);
//! for mailbox in request
//! .send()
//! .await
//! .unwrap()
//! .unwrap_method_responses()
//! .pop()
//! .unwrap()
//! .unwrap_get_mailbox()
//! .unwrap()
//! .take_list()
//! {
//! println!("Changed mailbox: {:#?}", mailbox);
//! }
//!
//! // Delete the mailbox including any messages
//! client.mailbox_destroy(&mailbox_id, true).await.unwrap();
//!
//! // Open an EventSource connection with the JMAP server.
//! let mut stream = client
//! .event_source(
//! [
//! TypeState::Email,
//! TypeState::EmailDelivery,
//! TypeState::Mailbox,
//! TypeState::EmailSubmission,
//! TypeState::Identity,
//! ]
//! .into(),
//! false,
//! 60.into(),
//! None,
//! )
//! .await
//! .unwrap();
//!
//! // Consume events received over EventSource.
//! while let Some(event) = stream.next().await {
//! let changes = event.unwrap();
//! println!("-> Change id: {:?}", changes.id());
//! for account_id in changes.changed_accounts() {
//! println!(" Account {} has changes:", account_id);
//! if let Some(account_changes) = changes.changes(account_id) {
//! for (type_state, state_id) in account_changes {
//! println!(" Type {:?} has a new state {}.", type_state, state_id);
//! }
//! }
//! }
//! }
//! ```
//!
//! More examples available under the [examples](examples) directory.
//!
//! ## Testing
//!
//! To run the testsuite:
//!
//! ```bash
//! $ cargo test --all-features
//! ```
//!
//! ## Conformed RFCs
//!
//! - [RFC 8620 - The JSON Meta Application Protocol (JMAP)](https://datatracker.ietf.org/doc/html/rfc8620)
//! - [RFC 8621 - The JSON Meta Application Protocol (JMAP) for Mail](https://datatracker.ietf.org/doc/html/rfc8621)
//! - [RFC 8887 - A JSON Meta Application Protocol (JMAP) Subprotocol for WebSocket](https://datatracker.ietf.org/doc/html/rfc8887)
//!
//! ## License
//!
//! Licensed under either of
//!
//! * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
//! * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
//!
//! at your option.
//!
//! ## Copyright
//!
//! Copyright (C) 2022, Stalwart Labs Ltd.
//!
#[forbid(unsafe_code)]
pub mod blob; pub mod blob;
pub mod client; pub mod client;
pub mod core; pub mod core;
@ -30,6 +194,13 @@ pub mod push_subscription;
pub mod thread; pub mod thread;
pub mod vacation_response; pub mod vacation_response;
use crate::core::error::MethodError;
use crate::core::error::ProblemDetails;
use crate::core::set::SetError;
use ahash::AHashMap;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[cfg(feature = "websockets")] #[cfg(feature = "websockets")]
pub mod client_ws; pub mod client_ws;