diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..063a9d7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "jmap-client" +description = "JMAP client library for Rust" +version = "0.1.0" +edition = "2018" +authors = [ "Stalwart Labs Ltd. "] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/stalwartlabs/jmap-client" +homepage = "https://github.com/stalwartlabs/jmap-client" +keywords = ["jmap", "email", "mime", "mail", "e-mail"] +categories = ["email"] +readme = "README.md" + +[dependencies] +serde = { version = "1.0", features = ["derive"]} +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"]} +reqwest = "0.11" +ece = "2.2" + +#[dev-dependencies] + +[profile.bench] +debug = true \ No newline at end of file diff --git a/src/blob/copy.rs b/src/blob/copy.rs new file mode 100644 index 0000000..ebb8ca6 --- /dev/null +++ b/src/blob/copy.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::core::set::SetError; + +#[derive(Debug, Clone, Serialize)] +pub struct CopyBlobRequest { + #[serde(rename = "fromAccountId")] + from_account_id: String, + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "blobIds")] + blob_ids: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CopyBlobResponse { + #[serde(rename = "fromAccountId")] + from_account_id: String, + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "copied")] + copied: Option>, + #[serde(rename = "notCopied")] + not_copied: Option>>, +} diff --git a/src/blob/mod.rs b/src/blob/mod.rs new file mode 100644 index 0000000..3da8034 --- /dev/null +++ b/src/blob/mod.rs @@ -0,0 +1 @@ +pub mod copy; diff --git a/src/core/changes.rs b/src/core/changes.rs new file mode 100644 index 0000000..6920990 --- /dev/null +++ b/src/core/changes.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize)] +pub struct ChangesRequest { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "sinceState")] + since_state: String, + #[serde(rename = "maxChanges")] + max_changes: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ChangesResponse { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "oldState")] + old_state: String, + #[serde(rename = "newState")] + new_state: String, + #[serde(rename = "hasMoreChanges")] + has_more_changes: bool, + created: Vec, + updated: Vec, + destroyed: Vec, +} diff --git a/src/core/copy.rs b/src/core/copy.rs new file mode 100644 index 0000000..3e61a87 --- /dev/null +++ b/src/core/copy.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::set::SetError; + +#[derive(Debug, Clone, Serialize)] +pub struct CopyRequest { + #[serde(rename = "fromAccountId")] + from_account_id: String, + #[serde(rename = "ifFromInState")] + if_from_in_state: Option, + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "ifInState")] + if_in_state: Option, + #[serde(rename = "create")] + create: HashMap, + #[serde(rename = "onSuccessDestroyOriginal")] + on_success_destroy_original: bool, + #[serde(rename = "destroyFromIfInState")] + destroy_from_if_in_state: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CopyResponse { + #[serde(rename = "fromAccountId")] + from_account_id: String, + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "oldState")] + old_state: Option, + #[serde(rename = "newState")] + new_state: String, + #[serde(rename = "created")] + created: Option>, + #[serde(rename = "notCreated")] + not_created: Option>>, +} diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..8a4a7e2 --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct ProblemDetails { + #[serde(rename = "type")] + p_type: ProblemType, + status: Option, + title: Option, + detail: Option, + limit: Option, +} + +#[derive(Debug, Deserialize)] +pub enum ProblemType { + #[serde(rename = "urn:ietf:params:jmap:error:unknownCapability")] + UnknownCapability, + #[serde(rename = "urn:ietf:params:jmap:error:notJSON")] + NotJSON, + #[serde(rename = "urn:ietf:params:jmap:error:notRequest")] + NotRequest, + #[serde(rename = "urn:ietf:params:jmap:error:limit")] + Limit, +} + +#[derive(Debug, Deserialize)] +pub struct MethodError { + #[serde(rename = "type")] + p_type: MethodErrorType, +} + +#[derive(Debug, Deserialize)] +pub enum MethodErrorType { + #[serde(rename = "serverUnavailable")] + ServerUnavailable, + #[serde(rename = "serverFail")] + ServerFail, + #[serde(rename = "serverPartialFail")] + ServerPartialFail, + #[serde(rename = "unknownMethod")] + UnknownMethod, + #[serde(rename = "invalidArguments")] + InvalidArguments, + #[serde(rename = "invalidResultReference")] + InvalidResultReference, + #[serde(rename = "forbidden")] + Forbidden, + #[serde(rename = "accountNotFound")] + AccountNotFound, + #[serde(rename = "accountNotSupportedByMethod")] + AccountNotSupportedByMethod, + #[serde(rename = "accountReadOnly")] + AccountReadOnly, + #[serde(rename = "requestTooLarge")] + RequestTooLarge, + #[serde(rename = "cannotCalculateChanges")] + CannotCalculateChanges, + #[serde(rename = "stateMismatch")] + StateMismatch, + #[serde(rename = "alreadyExists")] + AlreadyExists, + #[serde(rename = "fromAccountNotFound")] + FromAccountNotFound, + #[serde(rename = "fromAccountNotSupportedByMethod")] + FromAccountNotSupportedByMethod, + #[serde(rename = "anchorNotFound")] + AnchorNotFound, + #[serde(rename = "unsupportedSort")] + UnsupportedSort, + #[serde(rename = "unsupportedFilter")] + UnsupportedFilter, + #[serde(rename = "tooManyChanges")] + TooManyChanges, +} diff --git a/src/core/get.rs b/src/core/get.rs new file mode 100644 index 0000000..2916888 --- /dev/null +++ b/src/core/get.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize)] +pub struct GetRequest { + #[serde(rename = "accountId")] + account_id: String, + ids: Option>, + properties: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetResponse { + #[serde(rename = "accountId")] + account_id: String, + state: String, + list: Vec, + #[serde(rename = "notFound")] + not_found: Vec, +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..b77721f --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,10 @@ +pub mod changes; +pub mod copy; +pub mod error; +pub mod get; +pub mod query; +pub mod query_changes; +pub mod request; +pub mod response; +pub mod session; +pub mod set; diff --git a/src/core/query.rs b/src/core/query.rs new file mode 100644 index 0000000..46003ee --- /dev/null +++ b/src/core/query.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize)] +pub struct QueryRequest { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "filter")] + filter: Option>, + #[serde(rename = "sort")] + sort: Option>, + #[serde(rename = "position")] + position: i32, + #[serde(rename = "anchor")] + anchor: Option, + #[serde(rename = "anchorOffset")] + anchor_offset: i32, + #[serde(rename = "limit")] + limit: Option, + #[serde(rename = "calculateTotal")] + calculate_total: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum Filter { + FilterOperator(FilterOperator), + FilterCondition(T), +} + +#[derive(Debug, Clone, Serialize)] +pub struct FilterOperator { + operator: Operator, + conditions: Vec>, +} + +#[derive(Debug, Clone, Serialize)] +pub enum Operator { + #[serde(rename = "AND")] + And, + #[serde(rename = "OR")] + Or, + #[serde(rename = "NOT")] + Not, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Comparator { + property: T, + #[serde(rename = "isAscending")] + is_ascending: bool, + collation: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct QueryResponse { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "queryState")] + query_state: String, + #[serde(rename = "canCalculateChanges")] + can_calculate_changes: bool, + #[serde(rename = "position")] + position: i32, + #[serde(rename = "ids")] + ids: Vec, + #[serde(rename = "total")] + total: Option, + #[serde(rename = "limit")] + limit: Option, +} diff --git a/src/core/query_changes.rs b/src/core/query_changes.rs new file mode 100644 index 0000000..9ed5dd3 --- /dev/null +++ b/src/core/query_changes.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use super::query::Filter; + +#[derive(Debug, Clone, Serialize)] +pub struct QueryChangesRequest { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "filter")] + filter: Option>, + #[serde(rename = "sort")] + sort: Option>, + #[serde(rename = "sinceQueryState")] + since_query_state: String, + #[serde(rename = "maxChanges")] + max_changes: Option, + #[serde(rename = "upToId")] + up_to_id: Option, + #[serde(rename = "calculateTotal")] + calculate_total: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct QueryChangesResponse { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "oldQueryState")] + old_query_state: String, + #[serde(rename = "newQueryState")] + new_query_state: String, + #[serde(rename = "total")] + total: Option, + #[serde(rename = "removed")] + removed: Vec, + #[serde(rename = "added")] + added: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AddedItem { + id: String, + index: usize, +} diff --git a/src/core/request.rs b/src/core/request.rs new file mode 100644 index 0000000..e087d75 --- /dev/null +++ b/src/core/request.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::{Method, URI}; + +#[derive(Debug, Clone, Serialize)] +pub struct Request { + using: Vec, + #[serde(rename = "methodCalls")] + method_calls: Vec<(Method, Arguments, String)>, + #[serde(rename = "createdIds")] + #[serde(skip_serializing_if = "Option::is_none")] + created_ids: Option>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ResultReference { + #[serde(rename = "resultOf")] + result_of: String, + name: String, + path: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Arguments {} diff --git a/src/core/response.rs b/src/core/response.rs new file mode 100644 index 0000000..6c06f02 --- /dev/null +++ b/src/core/response.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::Method; + +#[derive(Debug, Clone, Deserialize)] +pub struct Request { + #[serde(rename = "methodResponses")] + method_calls: Vec<(Method, Result, String)>, + #[serde(rename = "createdIds")] + created_ids: Option>, + #[serde(rename = "sessionState")] + session_state: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Result {} diff --git a/src/core/session.rs b/src/core/session.rs new file mode 100644 index 0000000..8c4e880 --- /dev/null +++ b/src/core/session.rs @@ -0,0 +1,94 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct Session { + #[serde(rename = "capabilities")] + capabilities: HashMap, + #[serde(rename = "accounts")] + accounts: HashMap, + #[serde(rename = "primaryAccounts")] + primary_accounts: HashMap, + #[serde(rename = "username")] + username: String, + #[serde(rename = "apiUrl")] + api_url: String, + #[serde(rename = "downloadUrl")] + download_url: String, + #[serde(rename = "uploadUrl")] + upload_url: String, + #[serde(rename = "eventSourceUrl")] + event_source_url: String, + #[serde(rename = "state")] + state: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Account { + #[serde(rename = "name")] + name: String, + #[serde(rename = "isPersonal")] + is_personal: bool, + #[serde(rename = "isReadOnly")] + is_read_only: bool, + #[serde(rename = "accountCapabilities")] + account_capabilities: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +enum Capabilities { + Core(CoreCapabilities), + Mail(MailCapabilities), + Submission(SubmissionCapabilities), + EmptyCapabilities(EmptyCapabilities), + Other(serde_json::Value), +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CoreCapabilities { + #[serde(rename = "maxSizeUpload")] + max_size_upload: usize, + #[serde(rename = "maxConcurrentUpload")] + max_concurrent_upload: usize, + #[serde(rename = "maxSizeRequest")] + max_size_request: usize, + #[serde(rename = "maxConcurrentRequests")] + max_concurrent_requests: usize, + #[serde(rename = "maxCallsInRequest")] + max_calls_in_request: usize, + #[serde(rename = "maxObjectsInGet")] + max_objects_in_get: usize, + #[serde(rename = "maxObjectsInSet")] + max_objects_in_set: usize, + #[serde(rename = "collationAlgorithms")] + collation_algorithms: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MailCapabilities { + #[serde(rename = "maxMailboxesPerEmail")] + max_mailboxes_per_email: Option, + #[serde(rename = "maxMailboxDepth")] + max_mailbox_depth: usize, + #[serde(rename = "maxSizeMailboxName")] + max_size_mailbox_name: usize, + #[serde(rename = "maxSizeAttachmentsPerEmail")] + max_size_attachments_per_email: usize, + #[serde(rename = "emailQuerySortOptions")] + email_query_sort_options: Vec, + #[serde(rename = "mayCreateTopLevelMailbox")] + may_create_top_level_mailbox: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SubmissionCapabilities { + #[serde(rename = "maxDelayedSend")] + max_delayed_send: usize, + #[serde(rename = "submissionExtensions")] + submission_extensions: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct EmptyCapabilities {} diff --git a/src/core/set.rs b/src/core/set.rs new file mode 100644 index 0000000..caa0247 --- /dev/null +++ b/src/core/set.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::hash::Hash; + +#[derive(Debug, Clone, Serialize)] +pub struct SetRequest +where + U: Eq + Hash, +{ + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "ifInState")] + if_in_state: Option, + create: Option>, + update: Option>>, + destroy: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SetResponse { + #[serde(rename = "accountId")] + account_id: String, + #[serde(rename = "oldState")] + old_state: Option, + #[serde(rename = "newState")] + new_state: String, + #[serde(rename = "created")] + created: Option>, + #[serde(rename = "updated")] + updated: Option>>, + #[serde(rename = "destroyed")] + destroyed: Option>, + #[serde(rename = "notCreated")] + not_created: Option>>, + #[serde(rename = "notUpdated")] + not_updated: Option>>, + #[serde(rename = "notDestroyed")] + not_destroyed: Option>>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SetError { + #[serde(rename = "type")] + type_: SetErrorType, + description: Option, + properties: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub enum SetErrorType { + #[serde(rename = "forbidden")] + Forbidden, + #[serde(rename = "overQuota")] + OverQuota, + #[serde(rename = "tooLarge")] + TooLarge, + #[serde(rename = "rateLimit")] + RateLimit, + #[serde(rename = "notFound")] + NotFound, + #[serde(rename = "invalidPatch")] + InvalidPatch, + #[serde(rename = "willDestroy")] + WillDestroy, + #[serde(rename = "invalidProperties")] + InvalidProperties, + #[serde(rename = "singleton")] + Singleton, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b917968 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +pub mod blob; +pub mod core; +pub mod push_subscription; + +#[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, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +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 = "error")] + Error, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)] +pub enum Object { + Core, + Mailbox, + Thread, + Email, + SearchSnippet, + Identity, + EmailSubmission, + VacationResponse, + PushSubscription, +} + +#[derive(Deserialize)] +pub enum StateChangeType { + StateChange, +} + +#[derive(Deserialize)] +pub struct StateChange { + #[serde(rename(serialize = "@type"))] + pub type_: StateChangeType, + pub changed: HashMap>, +} diff --git a/src/push_subscription/get.rs b/src/push_subscription/get.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/push_subscription/mod.rs b/src/push_subscription/mod.rs new file mode 100644 index 0000000..1976b72 --- /dev/null +++ b/src/push_subscription/mod.rs @@ -0,0 +1,62 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::Object; + +pub mod get; +pub mod set; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PushSubscription { + #[serde(rename = "id")] + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + + #[serde(rename = "deviceClientId")] + #[serde(skip_serializing_if = "Option::is_none")] + device_client_id: Option, + + #[serde(rename = "url")] + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, + + #[serde(rename = "keys")] + #[serde(skip_serializing_if = "Option::is_none")] + keys: Option, + + #[serde(rename = "verificationCode")] + #[serde(skip_serializing_if = "Option::is_none")] + verification_code: Option, + + #[serde(rename = "expires")] + #[serde(skip_serializing_if = "Option::is_none")] + expires: Option>, + + #[serde(rename = "types")] + #[serde(skip_serializing_if = "Option::is_none")] + types: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PushSubscriptionProperty { + #[serde(rename = "id")] + Id, + #[serde(rename = "deviceClientId")] + DeviceClientId, + #[serde(rename = "url")] + Url, + #[serde(rename = "keys")] + Keys, + #[serde(rename = "verificationCode")] + VerificationCode, + #[serde(rename = "expires")] + Expires, + #[serde(rename = "types")] + Types, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Keys { + p256dh: String, + auth: String, +} diff --git a/src/push_subscription/set.rs b/src/push_subscription/set.rs new file mode 100644 index 0000000..e69de29