v0.1.0
parent
5e9c4f825e
commit
2e70d350ed
|
@ -0,0 +1,3 @@
|
||||||
|
jmap-client 0.1.0
|
||||||
|
================================
|
||||||
|
- First release.
|
171
README.md
171
README.md
|
@ -1,4 +1,169 @@
|
||||||
JMAP Client for Rust
|
# jmap-client
|
||||||
====
|
|
||||||
|
[](https://crates.io/crates/jmap-client)
|
||||||
|
[](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
|
||||||
|
[](https://docs.rs/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)
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
183
src/lib.rs
183
src/lib.rs
|
@ -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;
|
//! [](https://crates.io/crates/jmap-client)
|
||||||
use ahash::AHashMap;
|
//! [](https://github.com/stalwartlabs/jmap-client/actions/workflows/rust.yml)
|
||||||
use serde::{Deserialize, Serialize};
|
//! [](https://docs.rs/jmap-client)
|
||||||
use std::fmt::Display;
|
//! [](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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue