449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
/*
|
|
* 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
|
|
* <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.
|
|
*/
|
|
|
|
//! # 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)).
|
|
//! - JMAP for Sieve Scripts ([DRAFT-SIEVE-12](https://www.ietf.org/archive/id/draft-ietf-jmap-sieve-12.html)).
|
|
//!
|
|
//! 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 client;
|
|
pub mod core;
|
|
pub mod email;
|
|
pub mod email_submission;
|
|
#[cfg(feature = "async")]
|
|
pub mod event_source;
|
|
pub mod identity;
|
|
pub mod mailbox;
|
|
pub mod principal;
|
|
pub mod push_subscription;
|
|
pub mod sieve;
|
|
pub mod thread;
|
|
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")]
|
|
pub mod client_ws;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
|
pub enum URI {
|
|
#[serde(rename = "urn:ietf:params:jmap:core")]
|
|
Core,
|
|
#[serde(rename = "urn:ietf:params:jmap:mail")]
|
|
Mail,
|
|
#[serde(rename = "urn:ietf:params:jmap:submission")]
|
|
Submission,
|
|
#[serde(rename = "urn:ietf:params:jmap:vacationresponse")]
|
|
VacationResponse,
|
|
#[serde(rename = "urn:ietf:params:jmap:contacts")]
|
|
Contacts,
|
|
#[serde(rename = "urn:ietf:params:jmap:calendars")]
|
|
Calendars,
|
|
#[serde(rename = "urn:ietf:params:jmap:websocket")]
|
|
WebSocket,
|
|
#[serde(rename = "urn:ietf:params:jmap:sieve")]
|
|
Sieve,
|
|
#[serde(rename = "urn:ietf:params:jmap:principals")]
|
|
Principals,
|
|
#[serde(rename = "urn:ietf:params:jmap:principals:owner")]
|
|
PrincipalsOwner,
|
|
}
|
|
|
|
impl AsRef<str> for URI {
|
|
fn as_ref(&self) -> &str {
|
|
match self {
|
|
URI::Core => "urn:ietf:params:jmap:core",
|
|
URI::Mail => "urn:ietf:params:jmap:mail",
|
|
URI::Submission => "urn:ietf:params:jmap:submission",
|
|
URI::VacationResponse => "urn:ietf:params:jmap:vacationresponse",
|
|
URI::Contacts => "urn:ietf:params:jmap:contacts",
|
|
URI::Calendars => "urn:ietf:params:jmap:calendars",
|
|
URI::WebSocket => "urn:ietf:params:jmap:websocket",
|
|
URI::Sieve => "urn:ietf:params:jmap:sieve",
|
|
URI::Principals => "urn:ietf:params:jmap:principals",
|
|
URI::PrincipalsOwner => "urn:ietf:params:jmap:principals:owner",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub enum Method {
|
|
#[serde(rename = "Core/echo")]
|
|
Echo,
|
|
#[serde(rename = "Blob/copy")]
|
|
CopyBlob,
|
|
#[serde(rename = "PushSubscription/get")]
|
|
GetPushSubscription,
|
|
#[serde(rename = "PushSubscription/set")]
|
|
SetPushSubscription,
|
|
#[serde(rename = "Mailbox/get")]
|
|
GetMailbox,
|
|
#[serde(rename = "Mailbox/changes")]
|
|
ChangesMailbox,
|
|
#[serde(rename = "Mailbox/query")]
|
|
QueryMailbox,
|
|
#[serde(rename = "Mailbox/queryChanges")]
|
|
QueryChangesMailbox,
|
|
#[serde(rename = "Mailbox/set")]
|
|
SetMailbox,
|
|
#[serde(rename = "Thread/get")]
|
|
GetThread,
|
|
#[serde(rename = "Thread/changes")]
|
|
ChangesThread,
|
|
#[serde(rename = "Email/get")]
|
|
GetEmail,
|
|
#[serde(rename = "Email/changes")]
|
|
ChangesEmail,
|
|
#[serde(rename = "Email/query")]
|
|
QueryEmail,
|
|
#[serde(rename = "Email/queryChanges")]
|
|
QueryChangesEmail,
|
|
#[serde(rename = "Email/set")]
|
|
SetEmail,
|
|
#[serde(rename = "Email/copy")]
|
|
CopyEmail,
|
|
#[serde(rename = "Email/import")]
|
|
ImportEmail,
|
|
#[serde(rename = "Email/parse")]
|
|
ParseEmail,
|
|
#[serde(rename = "SearchSnippet/get")]
|
|
GetSearchSnippet,
|
|
#[serde(rename = "Identity/get")]
|
|
GetIdentity,
|
|
#[serde(rename = "Identity/changes")]
|
|
ChangesIdentity,
|
|
#[serde(rename = "Identity/set")]
|
|
SetIdentity,
|
|
#[serde(rename = "EmailSubmission/get")]
|
|
GetEmailSubmission,
|
|
#[serde(rename = "EmailSubmission/changes")]
|
|
ChangesEmailSubmission,
|
|
#[serde(rename = "EmailSubmission/query")]
|
|
QueryEmailSubmission,
|
|
#[serde(rename = "EmailSubmission/queryChanges")]
|
|
QueryChangesEmailSubmission,
|
|
#[serde(rename = "EmailSubmission/set")]
|
|
SetEmailSubmission,
|
|
#[serde(rename = "VacationResponse/get")]
|
|
GetVacationResponse,
|
|
#[serde(rename = "VacationResponse/set")]
|
|
SetVacationResponse,
|
|
#[serde(rename = "SieveScript/get")]
|
|
GetSieveScript,
|
|
#[serde(rename = "SieveScript/set")]
|
|
SetSieveScript,
|
|
#[serde(rename = "SieveScript/query")]
|
|
QuerySieveScript,
|
|
#[serde(rename = "SieveScript/validate")]
|
|
ValidateSieveScript,
|
|
#[serde(rename = "Principal/get")]
|
|
GetPrincipal,
|
|
#[serde(rename = "Principal/changes")]
|
|
ChangesPrincipal,
|
|
#[serde(rename = "Principal/query")]
|
|
QueryPrincipal,
|
|
#[serde(rename = "Principal/queryChanges")]
|
|
QueryChangesPrincipal,
|
|
#[serde(rename = "Principal/set")]
|
|
SetPrincipal,
|
|
#[serde(rename = "error")]
|
|
Error,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)]
|
|
pub enum TypeState {
|
|
Mailbox,
|
|
Thread,
|
|
Email,
|
|
EmailDelivery,
|
|
Identity,
|
|
EmailSubmission,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub enum StateChangeType {
|
|
StateChange,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct StateChange {
|
|
#[serde(rename = "@type")]
|
|
pub type_: StateChangeType,
|
|
pub changed: AHashMap<String, AHashMap<TypeState, String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Get;
|
|
#[derive(Debug, Clone)]
|
|
pub struct Set;
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
Transport(reqwest::Error),
|
|
Parse(serde_json::Error),
|
|
Internal(String),
|
|
Problem(Box<ProblemDetails>),
|
|
Server(String),
|
|
Method(MethodError),
|
|
Set(SetError<String>),
|
|
#[cfg(feature = "websockets")]
|
|
WebSocket(tokio_tungstenite::tungstenite::error::Error),
|
|
}
|
|
|
|
impl std::error::Error for Error {}
|
|
|
|
impl From<reqwest::Error> for Error {
|
|
fn from(e: reqwest::Error) -> Self {
|
|
Error::Transport(e)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for Error {
|
|
fn from(e: serde_json::Error) -> Self {
|
|
Error::Parse(e)
|
|
}
|
|
}
|
|
|
|
impl From<MethodError> for Error {
|
|
fn from(e: MethodError) -> Self {
|
|
Error::Method(e)
|
|
}
|
|
}
|
|
|
|
impl From<ProblemDetails> for Error {
|
|
fn from(e: ProblemDetails) -> Self {
|
|
Error::Problem(Box::new(e))
|
|
}
|
|
}
|
|
|
|
impl From<SetError<String>> for Error {
|
|
fn from(e: SetError<String>) -> Self {
|
|
Error::Set(e)
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Error {
|
|
fn from(s: &str) -> Self {
|
|
Error::Internal(s.to_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "websockets")]
|
|
impl From<tokio_tungstenite::tungstenite::error::Error> for Error {
|
|
fn from(e: tokio_tungstenite::tungstenite::error::Error) -> Self {
|
|
Error::WebSocket(e)
|
|
}
|
|
}
|
|
|
|
impl Display for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Error::Transport(e) => write!(f, "Transport error: {}", e),
|
|
Error::Parse(e) => write!(f, "Parse error: {}", e),
|
|
Error::Internal(e) => write!(f, "Internal error: {}", e),
|
|
Error::Problem(e) => write!(f, "Request failed: {}", e),
|
|
Error::Server(e) => write!(f, "Server failed: {}", e),
|
|
Error::Method(e) => write!(f, "Request failed: {}", e),
|
|
Error::Set(e) => write!(f, "Set failed: {}", e),
|
|
#[cfg(feature = "websockets")]
|
|
Error::WebSocket(e) => write!(f, "WebSockets error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for TypeState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
TypeState::Mailbox => write!(f, "Mailbox"),
|
|
TypeState::Thread => write!(f, "Thread"),
|
|
TypeState::Email => write!(f, "Email"),
|
|
TypeState::EmailDelivery => write!(f, "EmailDelivery"),
|
|
TypeState::Identity => write!(f, "Identity"),
|
|
TypeState::EmailSubmission => write!(f, "EmailSubmission"),
|
|
}
|
|
}
|
|
}
|