From e10834ecaa1d2efad1df5a5d05b256a8ebfbce64 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Wed, 11 May 2022 16:52:35 +0000 Subject: [PATCH] Complete implementation (untested) except SearchSnippet and EventSource. --- src/blob/download.rs | 53 +++ src/blob/mod.rs | 23 ++ src/blob/upload.rs | 85 +++++ src/client.rs | 204 +++++++++++- src/core/changes.rs | 9 + src/core/copy.rs | 23 +- src/core/error.rs | 27 ++ src/core/get.rs | 22 ++ src/core/query.rs | 4 + src/core/query_changes.rs | 10 + src/core/request.rs | 62 +++- src/core/response.rs | 628 ++++++++++++++++++++++++++++++++++- src/core/session.rs | 56 +++- src/core/set.rs | 139 +++++--- src/email/import.rs | 41 ++- src/email/mod.rs | 10 +- src/email/parse.rs | 4 +- src/email/set.rs | 67 ++-- src/email_submission/mod.rs | 3 + src/email_submission/set.rs | 23 +- src/event_source/mod.rs | 18 + src/identity/mod.rs | 3 + src/identity/set.rs | 23 +- src/lib.rs | 38 ++- src/mailbox/mod.rs | 15 + src/mailbox/query.rs | 14 +- src/mailbox/set.rs | 32 +- src/push_subscription/mod.rs | 3 + src/push_subscription/set.rs | 32 +- src/vacation_response/mod.rs | 3 + src/vacation_response/set.rs | 32 +- 31 files changed, 1540 insertions(+), 166 deletions(-) create mode 100644 src/blob/download.rs create mode 100644 src/blob/upload.rs create mode 100644 src/event_source/mod.rs diff --git a/src/blob/download.rs b/src/blob/download.rs new file mode 100644 index 0000000..a472987 --- /dev/null +++ b/src/blob/download.rs @@ -0,0 +1,53 @@ +use std::time::Duration; + +use reqwest::header::CONTENT_TYPE; + +use crate::{client::Client, core::session::URLPart}; + +impl Client { + pub async fn download(&self, account_id: &str, blob_id: &str) -> crate::Result> { + let mut download_url = String::with_capacity( + self.session().download_url().len() + account_id.len() + blob_id.len(), + ); + + for part in self.download_url() { + match part { + URLPart::Value(value) => { + download_url.push_str(value); + } + URLPart::Parameter(param) => match param { + super::URLParameter::AccountId => { + download_url.push_str(account_id); + } + super::URLParameter::BlobId => { + download_url.push_str(blob_id); + } + super::URLParameter::Name => { + download_url.push_str("none"); + } + super::URLParameter::Type => { + download_url.push_str("application/octet-stream"); + } + }, + } + } + + let mut headers = self.headers().clone(); + headers.remove(CONTENT_TYPE); + + Client::handle_error( + reqwest::Client::builder() + .timeout(Duration::from_millis(self.timeout())) + .default_headers(headers) + .build()? + .get(download_url) + .send() + .await?, + ) + .await? + .bytes() + .await + .map(|bytes| bytes.to_vec()) + .map_err(|err| err.into()) + } +} diff --git a/src/blob/mod.rs b/src/blob/mod.rs index 3da8034..4b30f2e 100644 --- a/src/blob/mod.rs +++ b/src/blob/mod.rs @@ -1 +1,24 @@ +use crate::core::session::URLParser; + pub mod copy; +pub mod download; +pub mod upload; + +pub enum URLParameter { + AccountId, + BlobId, + Name, + Type, +} + +impl URLParser for URLParameter { + fn parse(value: &str) -> Option { + match value { + "accountId" => Some(URLParameter::AccountId), + "blobId" => Some(URLParameter::BlobId), + "name" => Some(URLParameter::Name), + "type" => Some(URLParameter::Type), + _ => None, + } + } +} diff --git a/src/blob/upload.rs b/src/blob/upload.rs new file mode 100644 index 0000000..54d2f96 --- /dev/null +++ b/src/blob/upload.rs @@ -0,0 +1,85 @@ +use std::time::Duration; + +use reqwest::header::CONTENT_TYPE; +use serde::Deserialize; + +use crate::{client::Client, core::session::URLPart}; + +#[derive(Debug, Deserialize)] +pub struct UploadResponse { + #[serde(rename = "accountId")] + account_id: String, + + #[serde(rename = "blobId")] + blob_id: String, + + #[serde(rename = "type")] + type_: String, + + #[serde(rename = "size")] + size: usize, +} + +impl Client { + pub async fn upload( + &self, + account_id: &str, + content_type: Option<&str>, + blob: Vec, + ) -> crate::Result { + let mut upload_url = + String::with_capacity(self.session().upload_url().len() + account_id.len()); + + for part in self.upload_url() { + match part { + URLPart::Value(value) => { + upload_url.push_str(value); + } + URLPart::Parameter(param) => { + if let super::URLParameter::AccountId = param { + upload_url.push_str(account_id); + } + } + } + } + + serde_json::from_slice::( + &Client::handle_error( + reqwest::Client::builder() + .timeout(Duration::from_millis(self.timeout())) + .default_headers(self.headers().clone()) + .build()? + .post(upload_url) + .header( + CONTENT_TYPE, + content_type.unwrap_or("application/octet-stream"), + ) + .body(blob) + .send() + .await?, + ) + .await? + .bytes() + .await?, + ) + .map_err(|err| err.into()) + } +} + +impl UploadResponse { + pub fn account_id(&self) -> &str { + &self.account_id + } + + pub fn blob_id(&self) -> &str { + &self.blob_id + } + + pub fn content_type(&self) -> &str { + &self.type_ + } + + pub fn size(&self) -> usize { + self.size + } +} diff --git a/src/client.rs b/src/client.rs index 0fc72c7..7987f71 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,36 +1,226 @@ use std::time::Duration; -use crate::core::request::Request; +use reqwest::{ + header::{self}, + Response, +}; + +use crate::{ + blob, + core::{ + request::Request, + session::{Session, URLPart}, + }, + event_source, Error, +}; const DEFAULT_TIMEOUT_MS: u64 = 10 * 1000; +static USER_AGENT: &str = concat!("stalwart-jmap/", env!("CARGO_PKG_VERSION")); pub struct Client { - client: reqwest::ClientBuilder, + session: Session, + session_url: String, + upload_url: Vec>, + download_url: Vec>, + event_source_url: Vec>, + timeout: u64, + headers: header::HeaderMap, default_account_id: String, } impl Client { - pub fn connect(url: &str) -> Self { - Client { - client: reqwest::Client::builder().timeout(Duration::from_millis(DEFAULT_TIMEOUT_MS)), - default_account_id: "co".to_string(), + pub async fn connect(url: &str) -> crate::Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_static(USER_AGENT), + ); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_static("Basic test"), + ); + + let session: Session = serde_json::from_slice( + &Client::handle_error( + reqwest::Client::builder() + .timeout(Duration::from_millis(DEFAULT_TIMEOUT_MS)) + .default_headers(headers.clone()) + .build()? + .get(url) + .send() + .await?, + ) + .await? + .bytes() + .await?, + )?; + + let default_account_id = session + .primary_accounts() + .next() + .map(|a| a.0.to_string()) + .unwrap_or_default(); + + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + + Ok(Client { + download_url: URLPart::parse(session.download_url())?, + upload_url: URLPart::parse(session.upload_url())?, + event_source_url: URLPart::parse(session.event_source_url())?, + session, + session_url: url.to_string(), + timeout: DEFAULT_TIMEOUT_MS, + headers, + default_account_id, + }) + } + + pub fn set_timeout(&mut self, timeout: u64) { + self.timeout = timeout; + } + + pub fn timeout(&self) -> u64 { + self.timeout + } + + pub fn session(&self) -> &Session { + &self.session + } + + pub fn headers(&self) -> &header::HeaderMap { + &self.headers + } + + pub async fn update_session(&mut self, new_state: &str) -> crate::Result<()> { + if new_state != self.session.state() { + let session: Session = serde_json::from_slice( + &Client::handle_error( + reqwest::Client::builder() + .timeout(Duration::from_millis(DEFAULT_TIMEOUT_MS)) + .default_headers(self.headers.clone()) + .build()? + .get(&self.session_url) + .send() + .await?, + ) + .await? + .bytes() + .await?, + )?; + self.download_url = URLPart::parse(session.download_url())?; + self.upload_url = URLPart::parse(session.upload_url())?; + self.event_source_url = URLPart::parse(session.event_source_url())?; + self.session = session; } + Ok(()) + } + + pub fn set_default_account_id(&mut self, defaul_account_id: impl Into) { + self.default_account_id = defaul_account_id.into(); } pub fn default_account_id(&self) -> &str { &self.default_account_id } - pub fn request(&self) -> Request<'_> { + pub fn request(&mut self) -> Request<'_> { Request::new(self) } + + pub fn download_url(&self) -> &[URLPart] { + &self.download_url + } + + pub fn upload_url(&self) -> &[URLPart] { + &self.upload_url + } + + pub fn event_source_url(&self) -> &[URLPart] { + &self.event_source_url + } + + pub async fn handle_error(response: Response) -> crate::Result { + if response.status().is_success() { + Ok(response) + } else if let Some(b"application/problem+json") = response + .headers() + .get(header::CONTENT_TYPE) + .map(|h| h.as_bytes()) + { + Err(Error::Problem(serde_json::from_slice( + &response.bytes().await?, + )?)) + } else { + Err(Error::ServerError(format!("{}", response.status()))) + } + } } #[cfg(test)] mod tests { + use crate::core::response::Response; #[test] fn test_serialize() { + let r: Response = serde_json::from_slice( + br#"{"sessionState": "123", "methodResponses": [[ "Email/query", { + "accountId": "A1", + "queryState": "abcdefg", + "canCalculateChanges": true, + "position": 0, + "total": 101, + "ids": [ "msg1023", "msg223", "msg110", "msg93", "msg91", + "msg38", "msg36", "msg33", "msg11", "msg1" ] + }, "t0" ], + [ "Email/get", { + "accountId": "A1", + "state": "123456", + "list": [{ + "id": "msg1023", + "threadId": "trd194" + }, { + "id": "msg223", + "threadId": "trd114" + } + ], + "notFound": [] + }, "t1" ], + [ "Thread/get", { + "accountId": "A1", + "state": "123456", + "list": [{ + "id": "trd194", + "emailIds": [ "msg1020", "msg1021", "msg1023" ] + }, { + "id": "trd114", + "emailIds": [ "msg201", "msg223" ] + } + ], + "notFound": [] + }, "t2" ]]}"#, + ) + .unwrap(); + + println!("{:?}", r); + + /*let mut client = Client::connect("coco"); + let mut request = client.request(); + + let set = request.set_email(); + set.create().from(["pepe"]).subject("coco"); + set.update("id").keyword("keyword", true); + set.destroy(["1", "2"]); + + let ref_ = request.result_reference("/pepe/1"); + + let get = request.get_email(); + get.ids_ref(ref_); + + println!("{}", serde_json::to_string_pretty(&request).unwrap());*/ + /*let mut client = Client::connect("coco"); client.request().email_set().create( diff --git a/src/core/changes.rs b/src/core/changes.rs index 97f2b87..8f66259 100644 --- a/src/core/changes.rs +++ b/src/core/changes.rs @@ -4,9 +4,12 @@ use serde::{Deserialize, Serialize}; pub struct ChangesRequest { #[serde(rename = "accountId")] account_id: String, + #[serde(rename = "sinceState")] since_state: String, + #[serde(rename = "maxChanges")] + #[serde(skip_serializing_if = "Option::is_none")] max_changes: Option, } @@ -14,14 +17,20 @@ pub struct ChangesRequest { 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, #[serde(flatten)] diff --git a/src/core/copy.rs b/src/core/copy.rs index 94ce32d..77f0d3f 100644 --- a/src/core/copy.rs +++ b/src/core/copy.rs @@ -2,20 +2,22 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use super::set::SetError; +use super::set::{Create, SetError}; #[derive(Debug, Clone, Serialize)] -pub struct CopyRequest { +pub struct CopyRequest { #[serde(rename = "fromAccountId")] from_account_id: String, #[serde(rename = "ifFromInState")] + #[serde(skip_serializing_if = "Option::is_none")] if_from_in_state: Option, #[serde(rename = "accountId")] account_id: String, #[serde(rename = "ifInState")] + #[serde(skip_serializing_if = "Option::is_none")] if_in_state: Option, #[serde(rename = "create")] @@ -25,6 +27,7 @@ pub struct CopyRequest { on_success_destroy_original: bool, #[serde(rename = "destroyFromIfInState")] + #[serde(skip_serializing_if = "Option::is_none")] destroy_from_if_in_state: Option, } @@ -32,19 +35,24 @@ pub struct CopyRequest { 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>>, } -impl CopyRequest { +impl CopyRequest { pub fn new(from_account_id: String, account_id: String) -> Self { CopyRequest { from_account_id, @@ -72,9 +80,12 @@ impl CopyRequest { self } - pub fn create(&mut self, id: impl Into, value: T) -> &mut Self { - self.create.insert(id.into(), value); - self + pub fn create(&mut self) -> &mut T { + let create_id = self.create.len(); + let create_id_str = format!("c{}", create_id); + self.create + .insert(create_id_str.clone(), T::new(create_id.into())); + self.create.get_mut(&create_id_str).unwrap() } pub fn on_success_destroy_original(&mut self, on_success_destroy_original: bool) -> &mut Self { diff --git a/src/core/error.rs b/src/core/error.rs index e933c16..7f5b3f6 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -99,3 +101,28 @@ impl MethodError { &self.p_type } } + +impl Display for ProblemDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.p_type { + ProblemType::UnknownCapability => write!(f, "Unknown capability")?, + ProblemType::NotJSON => write!(f, "Not JSON")?, + ProblemType::NotRequest => write!(f, "Not request")?, + ProblemType::Limit => write!(f, "Limit")?, + } + + if let Some(status) = self.status { + write!(f, " (status {})", status)? + } + + if let Some(title) = &self.title { + write!(f, ": {}", title)? + } + + if let Some(detail) = &self.detail { + write!(f, ". Details: {}", detail)? + } + + Ok(()) + } +} diff --git a/src/core/get.rs b/src/core/get.rs index f3e8e67..f476ee3 100644 --- a/src/core/get.rs +++ b/src/core/get.rs @@ -1,10 +1,21 @@ use serde::{Deserialize, Serialize}; +use super::request::ResultReference; + #[derive(Debug, Clone, Serialize)] pub struct GetRequest { #[serde(rename = "accountId")] account_id: String, + + #[serde(skip_serializing_if = "Option::is_none")] ids: Option>, + + #[serde(rename = "#ids")] + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "Option::is_none")] + ids_ref: Option, + + #[serde(skip_serializing_if = "Option::is_none")] properties: Option>, #[serde(flatten)] @@ -15,8 +26,11 @@ pub struct GetRequest { pub struct GetResponse { #[serde(rename = "accountId")] account_id: String, + state: String, + list: Vec, + #[serde(rename = "notFound")] not_found: Vec, } @@ -26,6 +40,7 @@ impl GetRequest { GetRequest { account_id, ids: None, + ids_ref: None, properties: None, arguments: A::default(), } @@ -42,6 +57,13 @@ impl GetRequest { V: Into, { self.ids = Some(ids.into_iter().map(|v| v.into()).collect()); + self.ids_ref = None; + self + } + + pub fn ids_ref(&mut self, reference: ResultReference) -> &mut Self { + self.ids_ref = reference.into(); + self.ids = None; self } diff --git a/src/core/query.rs b/src/core/query.rs index a32f982..8def293 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -6,21 +6,25 @@ pub struct QueryRequest { account_id: String, #[serde(rename = "filter")] + #[serde(skip_serializing_if = "Option::is_none")] filter: Option>, #[serde(rename = "sort")] + #[serde(skip_serializing_if = "Option::is_none")] sort: Option>>, #[serde(rename = "position")] position: i32, #[serde(rename = "anchor")] + #[serde(skip_serializing_if = "Option::is_none")] anchor: Option, #[serde(rename = "anchorOffset")] anchor_offset: i32, #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] limit: Option, #[serde(rename = "calculateTotal")] diff --git a/src/core/query_changes.rs b/src/core/query_changes.rs index 51a334c..edba477 100644 --- a/src/core/query_changes.rs +++ b/src/core/query_changes.rs @@ -6,16 +6,26 @@ use super::query::{Comparator, Filter}; pub struct QueryChangesRequest { #[serde(rename = "accountId")] account_id: String, + #[serde(rename = "filter")] + #[serde(skip_serializing_if = "Option::is_none")] filter: Option>, + #[serde(rename = "sort")] + #[serde(skip_serializing_if = "Option::is_none")] sort: Option>>, + #[serde(rename = "sinceQueryState")] since_query_state: String, + #[serde(rename = "maxChanges")] + #[serde(skip_serializing_if = "Option::is_none")] max_changes: Option, + #[serde(rename = "upToId")] + #[serde(skip_serializing_if = "Option::is_none")] up_to_id: Option, + #[serde(rename = "calculateTotal")] calculate_total: bool, diff --git a/src/core/request.rs b/src/core/request.rs index 3154e28..add965f 100644 --- a/src/core/request.rs +++ b/src/core/request.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use serde::Serialize; @@ -17,17 +17,19 @@ use crate::{ use super::{ changes::ChangesRequest, copy::CopyRequest, get::GetRequest, query::QueryRequest, - query_changes::QueryChangesRequest, set::SetRequest, + query_changes::QueryChangesRequest, response::Response, set::SetRequest, }; #[derive(Serialize)] pub struct Request<'x> { #[serde(skip)] - client: &'x Client, + client: &'x mut Client, using: Vec, + #[serde(rename = "methodCalls")] method_calls: Vec<(Method, Arguments, String)>, + #[serde(rename = "createdIds")] #[serde(skip_serializing_if = "Option::is_none")] created_ids: Option>, @@ -37,7 +39,7 @@ pub struct Request<'x> { pub struct ResultReference { #[serde(rename = "resultOf")] result_of: String, - name: String, + name: Method, path: String, } @@ -392,7 +394,7 @@ impl Arguments { } impl<'x> Request<'x> { - pub fn new(client: &'x Client) -> Self { + pub fn new(client: &'x mut Client) -> Self { Request { using: vec![URI::Core, URI::Mail], method_calls: vec![], @@ -401,6 +403,26 @@ impl<'x> Request<'x> { } } + pub async fn send(&mut self) -> crate::Result { + let response: Response = serde_json::from_slice( + &Client::handle_error( + reqwest::Client::builder() + .timeout(Duration::from_millis(self.client.timeout())) + .default_headers(self.client.headers().clone()) + .build()? + .post(self.client.session().api_url()) + .body(serde_json::to_string(&self)?) + .send() + .await?, + ) + .await? + .bytes() + .await?, + )?; + self.client.update_session(response.session_state()).await?; + Ok(response) + } + fn add_method_call(&mut self, method: Method, arguments: Arguments) -> &mut Arguments { let call_id = format!("s{}", self.method_calls.len()); self.method_calls.push((method, arguments, call_id)); @@ -613,6 +635,9 @@ impl<'x> Request<'x> { } pub fn get_email_submission(&mut self) -> &mut GetRequest { + if !self.using.contains(&URI::Submission) { + self.using.push(URI::Submission); + } self.add_method_call( Method::GetEmailSubmission, Arguments::email_submission_get(self.client.default_account_id().to_string()), @@ -624,6 +649,9 @@ impl<'x> Request<'x> { &mut self, since_state: impl Into, ) -> &mut ChangesRequest { + if !self.using.contains(&URI::Submission) { + self.using.push(URI::Submission); + } self.add_method_call( Method::ChangesEmailSubmission, Arguments::changes( @@ -638,6 +666,9 @@ impl<'x> Request<'x> { &mut self, ) -> &mut QueryRequest { + if !self.using.contains(&URI::Submission) { + self.using.push(URI::Submission); + } self.add_method_call( Method::QueryEmailSubmission, Arguments::email_submission_query(self.client.default_account_id().to_string()), @@ -653,6 +684,9 @@ impl<'x> Request<'x> { email_submission::query::Comparator, (), > { + if !self.using.contains(&URI::Submission) { + self.using.push(URI::Submission); + } self.add_method_call( Method::QueryChangesEmailSubmission, Arguments::email_submission_query_changes( @@ -666,6 +700,9 @@ impl<'x> Request<'x> { pub fn set_email_submission( &mut self, ) -> &mut SetRequest, email_submission::SetArguments> { + if !self.using.contains(&URI::Submission) { + self.using.push(URI::Submission); + } self.add_method_call( Method::SetEmailSubmission, Arguments::email_submission_set(self.client.default_account_id().to_string()), @@ -674,6 +711,9 @@ impl<'x> Request<'x> { } pub fn get_vacation_response(&mut self) -> &mut GetRequest { + if !self.using.contains(&URI::VacationResponse) { + self.using.push(URI::VacationResponse); + } self.add_method_call( Method::GetVacationResponse, Arguments::vacation_response_get(self.client.default_account_id().to_string()), @@ -682,10 +722,22 @@ impl<'x> Request<'x> { } pub fn set_vacation_response(&mut self) -> &mut SetRequest, ()> { + if !self.using.contains(&URI::VacationResponse) { + self.using.push(URI::VacationResponse); + } self.add_method_call( Method::SetVacationResponse, Arguments::vacation_response_set(self.client.default_account_id().to_string()), ) .vacation_response_set_mut() } + + pub fn result_reference(&self, path: impl Into) -> ResultReference { + let last_method = self.method_calls.last().unwrap(); + ResultReference { + result_of: last_method.2.clone(), + name: last_method.0.clone(), + path: path.into(), + } + } } diff --git a/src/core/response.rs b/src/core/response.rs index 481adf1..8bdeec3 100644 --- a/src/core/response.rs +++ b/src/core/response.rs @@ -2,17 +2,635 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::Method; +use crate::{ + blob::copy::CopyBlobResponse, + email::{self, import::EmailImportResponse, parse::EmailParseResponse, Email}, + email_submission::{self, EmailSubmission}, + identity::{self, Identity}, + mailbox::{self, Mailbox}, + push_subscription::{self, PushSubscription}, + thread::Thread, + vacation_response::{self, VacationResponse}, + Get, Method, +}; -#[derive(Debug, Clone, Deserialize)] +use super::{ + changes::ChangesResponse, copy::CopyResponse, error::MethodError, get::GetResponse, + query::QueryResponse, query_changes::QueryChangesResponse, set::SetResponse, +}; + +#[derive(Debug, Deserialize)] pub struct Response { #[serde(rename = "methodResponses")] - method_calls: Vec<(Method, Result, String)>, + method_responses: Vec, + #[serde(rename = "createdIds")] created_ids: Option>, + #[serde(rename = "sessionState")] session_state: String, } -#[derive(Debug, Clone, Deserialize)] -pub struct Result {} +impl Response { + pub fn method_responses(&self) -> &[MethodResponse] { + self.method_responses.as_ref() + } + + pub fn method_response(&self, id: &str) -> Option<&MethodResponse> { + self.method_responses + .iter() + .find(|response| response.call_id() == id) + } + + pub fn created_ids(&self) -> Option> { + self.created_ids.as_ref().map(|map| map.iter()) + } + + pub fn session_state(&self) -> &str { + &self.session_state + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum MethodResponse { + CopyBlob((CopyBlob, CopyBlobResponse, String)), + GetPushSubscription( + ( + GetPushSubscription, + GetResponse>, + String, + ), + ), + SetPushSubscription( + ( + SetPushSubscription, + SetResponse, push_subscription::Property>, + String, + ), + ), + GetMailbox((GetMailbox, GetResponse>, String)), + ChangesMailbox( + ( + ChangesMailbox, + ChangesResponse, + String, + ), + ), + QueryMailbox((QueryMailbox, QueryResponse, String)), + QueryChangesMailbox((QueryChangesMailbox, QueryChangesResponse, String)), + SetMailbox( + ( + SetMailbox, + SetResponse, mailbox::Property>, + String, + ), + ), + GetThread((GetThread, GetResponse, String)), + ChangesThread((ChangesThread, ChangesResponse<()>, String)), + GetEmail((GetEmail, GetResponse>, String)), + ChangesEmail((ChangesEmail, ChangesResponse<()>, String)), + QueryEmail((QueryEmail, QueryResponse, String)), + QueryChangesEmail((QueryChangesEmail, QueryChangesResponse, String)), + SetEmail((SetEmail, SetResponse, email::Property>, String)), + CopyEmail((CopyEmail, CopyResponse, email::Property>, String)), + ImportEmail((ImportEmail, EmailImportResponse, String)), + ParseEmail((ParseEmail, EmailParseResponse, String)), + GetSearchSnippet((GetSearchSnippet, GetResponse, String)), + GetIdentity((GetIdentity, GetResponse>, String)), + ChangesIdentity((ChangesIdentity, ChangesResponse<()>, String)), + SetIdentity( + ( + SetIdentity, + SetResponse, identity::Property>, + String, + ), + ), + GetEmailSubmission( + ( + GetEmailSubmission, + GetResponse>, + String, + ), + ), + ChangesEmailSubmission((ChangesEmailSubmission, ChangesResponse<()>, String)), + QueryEmailSubmission((QueryEmailSubmission, QueryResponse, String)), + QueryChangesEmailSubmission((QueryChangesEmailSubmission, QueryChangesResponse, String)), + SetEmailSubmission( + ( + SetEmailSubmission, + SetResponse, email_submission::Property>, + String, + ), + ), + GetVacationResponse( + ( + GetVacationResponse, + GetResponse>, + String, + ), + ), + SetVacationResponse( + ( + SetVacationResponse, + SetResponse, vacation_response::Property>, + String, + ), + ), + Echo((Echo, serde_json::Value, String)), + Error((Error, MethodError, String)), +} + +impl MethodResponse { + pub fn call_id(&self) -> &str { + match self { + Self::CopyBlob((_, _, id)) => id, + Self::GetPushSubscription((_, _, id)) => id, + Self::SetPushSubscription((_, _, id)) => id, + Self::GetMailbox((_, _, id)) => id, + Self::ChangesMailbox((_, _, id)) => id, + Self::QueryMailbox((_, _, id)) => id, + Self::QueryChangesMailbox((_, _, id)) => id, + Self::SetMailbox((_, _, id)) => id, + Self::GetThread((_, _, id)) => id, + Self::ChangesThread((_, _, id)) => id, + Self::GetEmail((_, _, id)) => id, + Self::ChangesEmail((_, _, id)) => id, + Self::QueryEmail((_, _, id)) => id, + Self::QueryChangesEmail((_, _, id)) => id, + Self::SetEmail((_, _, id)) => id, + Self::CopyEmail((_, _, id)) => id, + Self::ImportEmail((_, _, id)) => id, + Self::ParseEmail((_, _, id)) => id, + Self::GetSearchSnippet((_, _, id)) => id, + Self::GetIdentity((_, _, id)) => id, + Self::ChangesIdentity((_, _, id)) => id, + Self::SetIdentity((_, _, id)) => id, + Self::GetEmailSubmission((_, _, id)) => id, + Self::ChangesEmailSubmission((_, _, id)) => id, + Self::QueryEmailSubmission((_, _, id)) => id, + Self::QueryChangesEmailSubmission((_, _, id)) => id, + Self::SetEmailSubmission((_, _, id)) => id, + Self::GetVacationResponse((_, _, id)) => id, + Self::SetVacationResponse((_, _, id)) => id, + Self::Echo((_, _, id)) => id, + Self::Error((_, _, id)) => id, + } + } + + pub fn is_type(&self, type_: Method) -> bool { + matches!( + (self, type_), + (Self::CopyBlob(_), Method::CopyBlob) + | (Self::GetPushSubscription(_), Method::GetPushSubscription) + | (Self::SetPushSubscription(_), Method::SetPushSubscription) + | (Self::GetMailbox(_), Method::GetMailbox) + | (Self::ChangesMailbox(_), Method::ChangesMailbox) + | (Self::QueryMailbox(_), Method::QueryMailbox) + | (Self::QueryChangesMailbox(_), Method::QueryChangesMailbox) + | (Self::SetMailbox(_), Method::SetMailbox) + | (Self::GetThread(_), Method::GetThread) + | (Self::ChangesThread(_), Method::ChangesThread) + | (Self::GetEmail(_), Method::GetEmail) + | (Self::ChangesEmail(_), Method::ChangesEmail) + | (Self::QueryEmail(_), Method::QueryEmail) + | (Self::QueryChangesEmail(_), Method::QueryChangesEmail) + | (Self::SetEmail(_), Method::SetEmail) + | (Self::CopyEmail(_), Method::CopyEmail) + | (Self::ImportEmail(_), Method::ImportEmail) + | (Self::ParseEmail(_), Method::ParseEmail) + | (Self::GetSearchSnippet(_), Method::GetSearchSnippet) + | (Self::GetIdentity(_), Method::GetIdentity) + | (Self::ChangesIdentity(_), Method::ChangesIdentity) + | (Self::SetIdentity(_), Method::SetIdentity) + | (Self::GetEmailSubmission(_), Method::GetEmailSubmission) + | ( + Self::ChangesEmailSubmission(_), + Method::ChangesEmailSubmission + ) + | (Self::QueryEmailSubmission(_), Method::QueryEmailSubmission) + | ( + Self::QueryChangesEmailSubmission(_), + Method::QueryChangesEmailSubmission + ) + | (Self::SetEmailSubmission(_), Method::SetEmailSubmission) + | (Self::GetVacationResponse(_), Method::GetVacationResponse) + | (Self::SetVacationResponse(_), Method::SetVacationResponse) + | (Self::Echo(_), Method::Echo) + | (Self::Error(_), Method::Error) + ) + } + + pub fn as_copy_copy(&self) -> Option<&CopyBlobResponse> { + match self { + Self::CopyBlob((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_push_subscription(&self) -> Option<&GetResponse>> { + match self { + Self::GetPushSubscription((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_push_subscription( + &self, + ) -> Option<&SetResponse, push_subscription::Property>> { + match self { + Self::SetPushSubscription((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_mailbox(&self) -> Option<&GetResponse>> { + match self { + Self::GetMailbox((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_changes_mailbox(&self) -> Option<&ChangesResponse> { + match self { + Self::ChangesMailbox((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_mailbox(&self) -> Option<&QueryResponse> { + match self { + Self::QueryMailbox((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_changes_mailbox(&self) -> Option<&QueryChangesResponse> { + match self { + Self::QueryChangesMailbox((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_mailbox(&self) -> Option<&SetResponse, mailbox::Property>> { + match self { + Self::SetMailbox((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_thread(&self) -> Option<&GetResponse> { + match self { + Self::GetThread((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_changes_thread(&self) -> Option<&ChangesResponse<()>> { + match self { + Self::ChangesThread((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_email(&self) -> Option<&GetResponse> { + match self { + Self::GetEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_changes_email(&self) -> Option<&ChangesResponse<()>> { + match self { + Self::ChangesEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_email(&self) -> Option<&QueryResponse> { + match self { + Self::QueryEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_changes_email(&self) -> Option<&QueryChangesResponse> { + match self { + Self::QueryChangesEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_email(&self) -> Option<&SetResponse> { + match self { + Self::SetEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_copy_email(&self) -> Option<&CopyResponse, email::Property>> { + match self { + Self::CopyEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_import_email(&self) -> Option<&EmailImportResponse> { + match self { + Self::ImportEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_parse_email(&self) -> Option<&EmailParseResponse> { + match self { + Self::ParseEmail((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_search_snippet(&self) -> Option<&GetResponse> { + match self { + Self::GetSearchSnippet((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_identity(&self) -> Option<&GetResponse> { + match self { + Self::GetIdentity((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_changes_identity(&self) -> Option<&ChangesResponse<()>> { + match self { + Self::ChangesIdentity((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_identity(&self) -> Option<&SetResponse> { + match self { + Self::SetIdentity((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_email_submission(&self) -> Option<&GetResponse> { + match self { + Self::GetEmailSubmission((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_changes_email_submission(&self) -> Option<&ChangesResponse<()>> { + match self { + Self::ChangesEmailSubmission((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_email_submission( + &self, + ) -> Option<&SetResponse> { + match self { + Self::SetEmailSubmission((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_email_submission(&self) -> Option<&QueryResponse> { + match self { + Self::QueryEmailSubmission((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_query_changes_email_submission(&self) -> Option<&QueryChangesResponse> { + match self { + Self::QueryChangesEmailSubmission((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_get_vacation_response(&self) -> Option<&GetResponse> { + match self { + Self::GetVacationResponse((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_set_vacation_response( + &self, + ) -> Option<&SetResponse> { + match self { + Self::SetVacationResponse((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_echo(&self) -> Option<&serde_json::Value> { + match self { + Self::Echo((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn as_error(&self) -> Option<&MethodError> { + match self { + Self::Error((_, response, _)) => response.into(), + _ => None, + } + } + + pub fn is_error(&self) -> bool { + matches!(self, Self::Error(_)) + } +} + +#[derive(Debug, Deserialize)] +pub enum Echo { + #[serde(rename = "Core/echo")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum CopyBlob { + #[serde(rename = "Blob/copy")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetPushSubscription { + #[serde(rename = "PushSubscription/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetPushSubscription { + #[serde(rename = "PushSubscription/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetMailbox { + #[serde(rename = "Mailbox/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ChangesMailbox { + #[serde(rename = "Mailbox/changes")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryMailbox { + #[serde(rename = "Mailbox/query")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryChangesMailbox { + #[serde(rename = "Mailbox/queryChanges")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetMailbox { + #[serde(rename = "Mailbox/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetThread { + #[serde(rename = "Thread/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ChangesThread { + #[serde(rename = "Thread/changes")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetEmail { + #[serde(rename = "Email/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ChangesEmail { + #[serde(rename = "Email/changes")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryEmail { + #[serde(rename = "Email/query")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryChangesEmail { + #[serde(rename = "Email/queryChanges")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetEmail { + #[serde(rename = "Email/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum CopyEmail { + #[serde(rename = "Email/copy")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ImportEmail { + #[serde(rename = "Email/import")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ParseEmail { + #[serde(rename = "Email/parse")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetSearchSnippet { + #[serde(rename = "SearchSnippet/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetIdentity { + #[serde(rename = "Identity/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ChangesIdentity { + #[serde(rename = "Identity/changes")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetIdentity { + #[serde(rename = "Identity/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetEmailSubmission { + #[serde(rename = "EmailSubmission/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum ChangesEmailSubmission { + #[serde(rename = "EmailSubmission/changes")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryEmailSubmission { + #[serde(rename = "EmailSubmission/query")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum QueryChangesEmailSubmission { + #[serde(rename = "EmailSubmission/queryChanges")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetEmailSubmission { + #[serde(rename = "EmailSubmission/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum GetVacationResponse { + #[serde(rename = "VacationResponse/get")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum SetVacationResponse { + #[serde(rename = "VacationResponse/set")] + V, +} + +#[derive(Debug, Deserialize)] +pub enum Error { + #[serde(rename = "error")] + V, +} diff --git a/src/core/session.rs b/src/core/session.rs index e241aa1..3f5a3fe 100644 --- a/src/core/session.rs +++ b/src/core/session.rs @@ -106,12 +106,8 @@ impl Session { self.accounts.get(account) } - pub fn primary_accounts(&self) -> impl Iterator { - self.primary_accounts.keys() - } - - pub fn primary_account(&self, account: &str) -> Option<&String> { - self.primary_accounts.get(account) + pub fn primary_accounts(&self) -> impl Iterator { + self.primary_accounts.iter() } pub fn username(&self) -> &str { @@ -194,3 +190,51 @@ impl CoreCapabilities { &self.collation_algorithms } } + +pub trait URLParser: Sized { + fn parse(value: &str) -> Option; +} + +pub enum URLPart { + Value(String), + Parameter(T), +} + +impl URLPart { + pub fn parse(url: &str) -> crate::Result>> { + let mut parts = Vec::new(); + let mut buf = String::with_capacity(url.len()); + let mut in_parameter = false; + + for ch in url.chars() { + match ch { + '{' => { + if !buf.is_empty() { + parts.push(URLPart::Value(buf.clone())); + buf.clear(); + } + in_parameter = true; + } + '}' => { + if in_parameter && !buf.is_empty() { + parts.push(URLPart::Parameter(T::parse(&buf).ok_or_else(|| { + crate::Error::Internal(format!( + "Invalid parameter '{}' in URL: {}", + buf, url + )) + })?)); + buf.clear(); + } else { + return Err(crate::Error::Internal(format!("Invalid URL: {}", url))); + } + in_parameter = false; + } + _ => { + buf.push(ch); + } + } + } + + Ok(parts) + } +} diff --git a/src/core/set.rs b/src/core/set.rs index 7acd2d2..f3a8e17 100644 --- a/src/core/set.rs +++ b/src/core/set.rs @@ -2,16 +2,31 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use super::request::ResultReference; + #[derive(Debug, Clone, Serialize)] -pub struct SetRequest { +pub struct SetRequest { #[serde(rename = "accountId")] account_id: String, + #[serde(rename = "ifInState")] + #[serde(skip_serializing_if = "Option::is_none")] if_in_state: Option, + + #[serde(skip_serializing_if = "Option::is_none")] create: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] update: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] destroy: Option>, + #[serde(rename = "#destroy")] + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "Option::is_none")] + destroy_ref: Option, + #[serde(flatten)] arguments: A, } @@ -20,20 +35,28 @@ pub struct SetRequest { 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>>, } @@ -94,23 +117,12 @@ pub enum SetErrorType { CannotUnsend, } -pub fn from_timestamp(timestamp: i64) -> DateTime { - DateTime::::from_utc(NaiveDateTime::from_timestamp(timestamp, 0), Utc) +pub trait Create: Sized { + fn new(create_id: Option) -> Self; + fn create_id(&self) -> Option; } -pub fn string_not_set(string: &Option) -> bool { - matches!(string, Some(string) if string.is_empty()) -} - -pub fn date_not_set(date: &Option>) -> bool { - matches!(date, Some(date) if date.timestamp() == 0) -} - -pub fn list_not_set(list: &Option>) -> bool { - matches!(list, Some(list) if list.is_empty() ) -} - -impl SetRequest { +impl SetRequest { pub fn new(account_id: String) -> Self { Self { account_id, @@ -118,6 +130,7 @@ impl SetRequest { create: None, update: None, destroy: None, + destroy_ref: None, arguments: Default::default(), } } @@ -132,22 +145,42 @@ impl SetRequest { self } - pub fn create(&mut self, id: impl Into, value: T) -> &mut Self { + pub fn create(&mut self) -> &mut T { + let create_id = self.create.as_ref().map_or(0, |c| c.len()); + let create_id_str = format!("c{}", create_id); self.create .get_or_insert_with(HashMap::new) - .insert(id.into(), value); - self + .insert(create_id_str.clone(), T::new(create_id.into())); + self.create + .as_mut() + .unwrap() + .get_mut(&create_id_str) + .unwrap() } - pub fn update(&mut self, id: impl Into, value: T) -> &mut Self { + pub fn update(&mut self, id: impl Into) -> &mut T { + let id: String = id.into(); self.update .get_or_insert_with(HashMap::new) - .insert(id.into(), value); + .insert(id.clone(), T::new(None)); + self.update.as_mut().unwrap().get_mut(&id).unwrap() + } + + pub fn destroy(&mut self, ids: U) -> &mut Self + where + U: IntoIterator, + V: Into, + { + self.destroy + .get_or_insert_with(Vec::new) + .extend(ids.into_iter().map(|id| id.into())); + self.destroy_ref = None; self } - pub fn destroy(&mut self, id: impl Into) -> &mut Self { - self.destroy.get_or_insert_with(Vec::new).push(id.into()); + pub fn destroy_ref(&mut self, reference: ResultReference) -> &mut Self { + self.destroy_ref = reference.into(); + self.destroy = None; self } @@ -169,40 +202,28 @@ impl SetResponse { &self.new_state } - pub fn created(&self) -> Option> { - self.created.as_ref().map(|map| map.keys()) + pub fn created(&self) -> Option> { + self.created.as_ref().map(|map| map.iter()) } - pub fn updated(&self) -> Option> { - self.updated.as_ref().map(|map| map.keys()) + pub fn updated(&self) -> Option)>> { + self.updated.as_ref().map(|map| map.iter()) } pub fn destroyed(&self) -> Option<&[String]> { self.destroyed.as_deref() } - pub fn not_created(&self) -> Option> { - self.not_created.as_ref().map(|map| map.keys()) + pub fn not_created(&self) -> Option)>> { + self.not_created.as_ref().map(|map| map.iter()) } - pub fn not_updated(&self) -> Option> { - self.not_updated.as_ref().map(|map| map.keys()) + pub fn not_updated(&self) -> Option)>> { + self.not_updated.as_ref().map(|map| map.iter()) } - pub fn not_destroyed(&self) -> Option> { - self.not_destroyed.as_ref().map(|map| map.keys()) - } - - pub fn not_created_reason(&self, id: &str) -> Option<&SetError> { - self.not_created.as_ref().and_then(|map| map.get(id)) - } - - pub fn not_updated_reason(&self, id: &str) -> Option<&SetError> { - self.not_updated.as_ref().and_then(|map| map.get(id)) - } - - pub fn not_destroyed_reason(&self, id: &str) -> Option<&SetError> { - self.not_destroyed.as_ref().and_then(|map| map.get(id)) + pub fn not_destroyed(&self) -> Option)>> { + self.not_destroyed.as_ref().map(|map| map.iter()) } pub fn created_details(&self, id: &str) -> Option<&T> { @@ -212,6 +233,18 @@ impl SetResponse { pub fn updated_details(&self, id: &str) -> Option<&T> { self.updated.as_ref().and_then(|map| map.get(id))?.as_ref() } + + pub fn not_created_details(&self, id: &str) -> Option<&SetError> { + self.not_created.as_ref().and_then(|map| map.get(id)) + } + + pub fn not_updated_details(&self, id: &str) -> Option<&SetError> { + self.not_updated.as_ref().and_then(|map| map.get(id)) + } + + pub fn not_destroyed_details(&self, id: &str) -> Option<&SetError> { + self.not_destroyed.as_ref().and_then(|map| map.get(id)) + } } impl SetError { @@ -227,3 +260,19 @@ impl SetError { self.properties.as_deref() } } + +pub fn from_timestamp(timestamp: i64) -> DateTime { + DateTime::::from_utc(NaiveDateTime::from_timestamp(timestamp, 0), Utc) +} + +pub fn string_not_set(string: &Option) -> bool { + matches!(string, Some(string) if string.is_empty()) +} + +pub fn date_not_set(date: &Option>) -> bool { + matches!(date, Some(date) if date.timestamp() == 0) +} + +pub fn list_not_set(list: &Option>) -> bool { + matches!(list, Some(list) if list.is_empty() ) +} diff --git a/src/email/import.rs b/src/email/import.rs index a8016ab..46c58c2 100644 --- a/src/email/import.rs +++ b/src/email/import.rs @@ -11,7 +11,9 @@ use super::{Email, Property}; pub struct EmailImportRequest { #[serde(rename = "accountId")] account_id: String, + #[serde(rename = "ifInState")] + #[serde(skip_serializing_if = "Option::is_none")] if_in_state: Option, emails: HashMap, @@ -19,6 +21,9 @@ pub struct EmailImportRequest { #[derive(Debug, Clone, Serialize)] pub struct EmailImport { + #[serde(skip)] + create_id: usize, + #[serde(rename = "blobId")] blob_id: String, @@ -37,12 +42,16 @@ pub struct EmailImport { pub struct EmailImportResponse { #[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>>, } @@ -56,20 +65,26 @@ impl EmailImportRequest { } } - pub fn if_in_state(&mut self, if_in_state: String) -> &mut Self { - self.if_in_state = Some(if_in_state); + pub fn if_in_state(&mut self, if_in_state: impl Into) -> &mut Self { + self.if_in_state = Some(if_in_state.into()); self } - pub fn add_email(&mut self, id: String, email_import: EmailImport) -> &mut Self { - self.emails.insert(id, email_import); - self + pub fn email(&mut self, blob_id: impl Into) -> &mut EmailImport { + let create_id = self.emails.len(); + let create_id_str = format!("i{}", create_id); + self.emails.insert( + create_id_str.clone(), + EmailImport::new(blob_id.into(), create_id), + ); + self.emails.get_mut(&create_id_str).unwrap() } } impl EmailImport { - pub fn new(blob_id: String) -> Self { + fn new(blob_id: String, create_id: usize) -> Self { EmailImport { + create_id, blob_id, mailbox_ids: HashMap::new(), keywords: HashMap::new(), @@ -77,20 +92,24 @@ impl EmailImport { } } - pub fn mailbox_id(mut self, mailbox_id: String) -> Self { - self.mailbox_ids.insert(mailbox_id, true); + pub fn mailbox_id(&mut self, mailbox_id: impl Into) -> &mut Self { + self.mailbox_ids.insert(mailbox_id.into(), true); self } - pub fn keyword(mut self, keyword: String) -> Self { - self.keywords.insert(keyword, true); + pub fn keyword(&mut self, keyword: impl Into) -> &mut Self { + self.keywords.insert(keyword.into(), true); self } - pub fn received_at(mut self, received_at: i64) -> Self { + pub fn received_at(&mut self, received_at: i64) -> &mut Self { self.received_at = Some(from_timestamp(received_at)); self } + + pub fn create_id(&self) -> String { + format!("i{}", self.create_id) + } } impl EmailImportResponse { diff --git a/src/email/mod.rs b/src/email/mod.rs index fe77eeb..b17d443 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -9,10 +9,13 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::Get; +use crate::{core::request::ResultReference, Get}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Email { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, @@ -32,6 +35,11 @@ pub struct Email { #[serde(skip_serializing_if = "Option::is_none")] mailbox_ids: Option>, + #[serde(rename = "#mailboxIds")] + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "Option::is_none")] + mailbox_ids_ref: Option, + #[serde(rename = "keywords")] #[serde(skip_serializing_if = "Option::is_none")] keywords: Option>, diff --git a/src/email/parse.rs b/src/email/parse.rs index b9ec5cd..6ec22a3 100644 --- a/src/email/parse.rs +++ b/src/email/parse.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::{BodyProperty, Email, Property}; @@ -31,7 +31,7 @@ pub struct EmailParseRequest { max_body_value_bytes: usize, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct EmailParseResponse { #[serde(rename = "accountId")] account_id: String, diff --git a/src/email/set.rs b/src/email/set.rs index c7a6799..b98c2ec 100644 --- a/src/email/set.rs +++ b/src/email/set.rs @@ -1,22 +1,35 @@ use std::collections::HashMap; -use crate::{core::set::from_timestamp, Set}; +use crate::{ + core::{ + request::ResultReference, + set::{from_timestamp, Create}, + }, + Set, +}; use super::{ Email, EmailAddress, EmailAddressGroup, EmailBodyPart, EmailBodyValue, EmailHeader, Field, }; impl Email { - pub fn mailbox_ids(mut self, mailbox_ids: T) -> Self + pub fn mailbox_ids(&mut self, mailbox_ids: T) -> &mut Self where T: IntoIterator, U: Into, { self.mailbox_ids = Some(mailbox_ids.into_iter().map(|s| (s.into(), true)).collect()); + self.mailbox_ids_ref = None; self } - pub fn mailbox_id(mut self, mailbox_id: &str, set: bool) -> Self { + pub fn mailbox_ids_ref(&mut self, reference: ResultReference) -> &mut Self { + self.mailbox_ids_ref = reference.into(); + self.mailbox_ids = None; + self + } + + pub fn mailbox_id(&mut self, mailbox_id: &str, set: bool) -> &mut Self { self.mailbox_ids = None; self.others.insert( format!("mailboxIds/{}", mailbox_id), @@ -25,7 +38,7 @@ impl Email { self } - pub fn keywords(mut self, keywords: T) -> Self + pub fn keywords(&mut self, keywords: T) -> &mut Self where T: IntoIterator, U: Into, @@ -34,14 +47,14 @@ impl Email { self } - pub fn keyword(mut self, keyword: &str, set: bool) -> Self { + pub fn keyword(&mut self, keyword: &str, set: bool) -> &mut Self { self.keywords = None; self.others .insert(format!("keywords/{}", keyword), Field::Bool(set).into()); self } - pub fn message_id(mut self, message_id: T) -> Self + pub fn message_id(&mut self, message_id: T) -> &mut Self where T: IntoIterator, U: Into, @@ -50,7 +63,7 @@ impl Email { self } - pub fn in_reply_to(mut self, in_reply_to: T) -> Self + pub fn in_reply_to(&mut self, in_reply_to: T) -> &mut Self where T: IntoIterator, U: Into, @@ -59,7 +72,7 @@ impl Email { self } - pub fn references(mut self, references: T) -> Self + pub fn references(&mut self, references: T) -> &mut Self where T: IntoIterator, U: Into, @@ -68,7 +81,7 @@ impl Email { self } - pub fn sender(mut self, sender: T) -> Self + pub fn sender(&mut self, sender: T) -> &mut Self where T: IntoIterator, U: Into, @@ -77,7 +90,7 @@ impl Email { self } - pub fn from(mut self, from: T) -> Self + pub fn from(&mut self, from: T) -> &mut Self where T: IntoIterator, U: Into, @@ -86,7 +99,7 @@ impl Email { self } - pub fn to(mut self, to: T) -> Self + pub fn to(&mut self, to: T) -> &mut Self where T: IntoIterator, U: Into, @@ -95,7 +108,7 @@ impl Email { self } - pub fn cc(mut self, cc: T) -> Self + pub fn cc(&mut self, cc: T) -> &mut Self where T: IntoIterator, U: Into, @@ -104,7 +117,7 @@ impl Email { self } - pub fn bcc(mut self, bcc: T) -> Self + pub fn bcc(&mut self, bcc: T) -> &mut Self where T: IntoIterator, U: Into, @@ -113,7 +126,7 @@ impl Email { self } - pub fn reply_to(mut self, reply_to: T) -> Self + pub fn reply_to(&mut self, reply_to: T) -> &mut Self where T: IntoIterator, U: Into, @@ -122,59 +135,61 @@ impl Email { self } - pub fn subject(mut self, subject: impl Into) -> Self { + pub fn subject(&mut self, subject: impl Into) -> &mut Self { self.subject = Some(subject.into()); self } - pub fn sent_at(mut self, sent_at: i64) -> Self { + pub fn sent_at(&mut self, sent_at: i64) -> &mut Self { self.sent_at = Some(from_timestamp(sent_at)); self } - pub fn body_structure(mut self, body_structure: EmailBodyPart) -> Self { + pub fn body_structure(&mut self, body_structure: EmailBodyPart) -> &mut Self { self.body_structure = Some(body_structure.into()); self } - pub fn body_value(mut self, id: String, body_value: impl Into) -> Self { + pub fn body_value(&mut self, id: String, body_value: impl Into) -> &mut Self { self.body_values .get_or_insert_with(HashMap::new) .insert(id, body_value.into()); self } - pub fn text_body(mut self, text_body: EmailBodyPart) -> Self { + pub fn text_body(&mut self, text_body: EmailBodyPart) -> &mut Self { self.text_body.get_or_insert_with(Vec::new).push(text_body); self } - pub fn html_body(mut self, html_body: EmailBodyPart) -> Self { + pub fn html_body(&mut self, html_body: EmailBodyPart) -> &mut Self { self.html_body.get_or_insert_with(Vec::new).push(html_body); self } - pub fn attachment(mut self, attachment: EmailBodyPart) -> Self { + pub fn attachment(&mut self, attachment: EmailBodyPart) -> &mut Self { self.attachments .get_or_insert_with(Vec::new) .push(attachment); self } - pub fn header(mut self, header: String, value: impl Into) -> Self { + pub fn header(&mut self, header: String, value: impl Into) -> &mut Self { self.others.insert(header, Some(value.into())); self } } -impl Email { - pub fn new() -> Email { +impl Create for Email { + fn new(_create_id: Option) -> Email { Email { + _create_id, _state: Default::default(), id: Default::default(), blob_id: Default::default(), thread_id: Default::default(), mailbox_ids: Default::default(), + mailbox_ids_ref: Default::default(), keywords: Default::default(), size: Default::default(), received_at: Default::default(), @@ -199,6 +214,10 @@ impl Email { others: Default::default(), } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } } impl EmailBodyPart { diff --git a/src/email_submission/mod.rs b/src/email_submission/mod.rs index f9728cc..d261be8 100644 --- a/src/email_submission/mod.rs +++ b/src/email_submission/mod.rs @@ -19,6 +19,9 @@ pub struct SetArguments { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailSubmission { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, diff --git a/src/email_submission/set.rs b/src/email_submission/set.rs index 2dedb52..ebeba9e 100644 --- a/src/email_submission/set.rs +++ b/src/email_submission/set.rs @@ -1,21 +1,21 @@ use std::collections::HashMap; -use crate::Set; +use crate::{core::set::Create, Set}; use super::{Address, EmailSubmission, Envelope, UndoStatus}; impl EmailSubmission { - pub fn identity_id(mut self, identity_id: String) -> Self { - self.identity_id = Some(identity_id); + pub fn identity_id(&mut self, identity_id: impl Into) -> &mut Self { + self.identity_id = Some(identity_id.into()); self } - pub fn email_id(mut self, email_id: String) -> Self { - self.email_id = Some(email_id); + pub fn email_id(&mut self, email_id: impl Into) -> &mut Self { + self.email_id = Some(email_id.into()); self } - pub fn envelope(mut self, mail_from: U, rcpt_to: T) -> Self + pub fn envelope(&mut self, mail_from: U, rcpt_to: T) -> &mut Self where T: Iterator, U: Into
, @@ -27,15 +27,16 @@ impl EmailSubmission { self } - pub fn undo_status(mut self, undo_status: UndoStatus) -> Self { + pub fn undo_status(&mut self, undo_status: UndoStatus) -> &mut Self { self.undo_status = Some(undo_status); self } } -impl EmailSubmission { - pub fn new() -> EmailSubmission { +impl Create for EmailSubmission { + fn new(_create_id: Option) -> Self { EmailSubmission { + _create_id, _state: Default::default(), id: None, identity_id: None, @@ -49,6 +50,10 @@ impl EmailSubmission { mdn_blob_ids: None, } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } } impl Address { diff --git a/src/event_source/mod.rs b/src/event_source/mod.rs new file mode 100644 index 0000000..9b45ae3 --- /dev/null +++ b/src/event_source/mod.rs @@ -0,0 +1,18 @@ +use crate::core::session::URLParser; + +pub enum URLParameter { + Types, + CloseAfter, + Ping, +} + +impl URLParser for URLParameter { + fn parse(value: &str) -> Option { + match value { + "types" => Some(URLParameter::Types), + "closeafter" => Some(URLParameter::CloseAfter), + "ping" => Some(URLParameter::Ping), + _ => None, + } + } +} diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 38f8271..7d3123d 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Identity { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, diff --git a/src/identity/set.rs b/src/identity/set.rs index f570b16..1b9d38b 100644 --- a/src/identity/set.rs +++ b/src/identity/set.rs @@ -1,19 +1,19 @@ -use crate::{email::EmailAddress, Set}; +use crate::{core::set::Create, email::EmailAddress, Set}; use super::Identity; impl Identity { - pub fn name(mut self, name: String) -> Self { + pub fn name(&mut self, name: String) -> &mut Self { self.name = Some(name); self } - pub fn email(mut self, email: String) -> Self { + pub fn email(&mut self, email: String) -> &mut Self { self.email = Some(email); self } - pub fn bcc(mut self, bcc: Option) -> Self + pub fn bcc(&mut self, bcc: Option) -> &mut Self where T: Iterator, U: Into, @@ -22,7 +22,7 @@ impl Identity { self } - pub fn reply_to(mut self, reply_to: Option) -> Self + pub fn reply_to(&mut self, reply_to: Option) -> &mut Self where T: Iterator, U: Into, @@ -31,20 +31,21 @@ impl Identity { self } - pub fn text_signature(mut self, text_signature: String) -> Self { + pub fn text_signature(&mut self, text_signature: String) -> &mut Self { self.text_signature = Some(text_signature); self } - pub fn html_signature(mut self, html_signature: String) -> Self { + pub fn html_signature(&mut self, html_signature: String) -> &mut Self { self.html_signature = Some(html_signature); self } } -impl Identity { - pub fn new() -> Identity { +impl Create for Identity { + fn new(_create_id: Option) -> Self { Identity { + _create_id, _state: Default::default(), id: None, name: None, @@ -56,4 +57,8 @@ impl Identity { may_delete: None, } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } } diff --git a/src/lib.rs b/src/lib.rs index 8b54d68..67635ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use crate::core::error::ProblemDetails; +use std::{collections::HashMap, fmt::Display}; use serde::{Deserialize, Serialize}; @@ -7,6 +8,7 @@ pub mod client; pub mod core; pub mod email; pub mod email_submission; +pub mod event_source; pub mod identity; pub mod mailbox; pub mod push_subscription; @@ -125,3 +127,37 @@ pub struct StateChange { pub struct Get; #[derive(Debug, Clone)] pub struct Set; + +pub type Result = std::result::Result; + +pub enum Error { + Transport(reqwest::Error), + Parse(serde_json::Error), + Internal(String), + Problem(ProblemDetails), + ServerError(String), +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Transport(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Parse(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, "Problem details: {}", e), + Error::ServerError(e) => write!(f, "Server error: {}", e), + } + } +} diff --git a/src/mailbox/mod.rs b/src/mailbox/mod.rs index 9d7dbde..8a85c91 100644 --- a/src/mailbox/mod.rs +++ b/src/mailbox/mod.rs @@ -21,8 +21,17 @@ pub struct QueryArguments { filter_as_tree: bool, } +#[derive(Debug, Deserialize, Default)] +pub struct ChangesResponse { + #[serde(rename = "updatedProperties")] + updated_properties: Option>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Mailbox { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, @@ -139,3 +148,9 @@ pub enum Property { #[serde(rename = "isSubscribed")] IsSubscribed, } + +impl ChangesResponse { + pub fn updated_properties(&self) -> Option<&[Property]> { + self.updated_properties.as_deref() + } +} diff --git a/src/mailbox/query.rs b/src/mailbox/query.rs index 5b5c4fb..df39e22 100644 --- a/src/mailbox/query.rs +++ b/src/mailbox/query.rs @@ -2,7 +2,7 @@ use serde::Serialize; use crate::core::query::{self}; -use super::Role; +use super::{QueryArguments, Role}; #[derive(Serialize, Clone, Debug)] #[serde(untagged)] @@ -81,3 +81,15 @@ impl Comparator { query::Comparator::new(Comparator::ParentId) } } + +impl QueryArguments { + pub fn sort_as_tree(&mut self, value: bool) -> &mut Self { + self.sort_as_tree = value; + self + } + + pub fn filter_as_tree(&mut self, value: bool) -> &mut Self { + self.filter_as_tree = value; + self + } +} diff --git a/src/mailbox/set.rs b/src/mailbox/set.rs index be8394c..389c9a0 100644 --- a/src/mailbox/set.rs +++ b/src/mailbox/set.rs @@ -1,19 +1,19 @@ -use crate::Set; +use crate::{core::set::Create, Set}; -use super::{Mailbox, Role}; +use super::{Mailbox, Role, SetArguments}; impl Mailbox { - pub fn name(mut self, name: String) -> Self { - self.name = Some(name); + pub fn name(&mut self, name: impl Into) -> &mut Self { + self.name = Some(name.into()); self } - pub fn parent_id(mut self, parent_id: Option) -> Self { - self.parent_id = parent_id; + pub fn parent_id(&mut self, parent_id: Option>) -> &mut Self { + self.parent_id = parent_id.map(|s| s.into()); self } - pub fn role(mut self, role: Role) -> Self { + pub fn role(&mut self, role: Role) -> &mut Self { if !matches!(role, Role::None) { self.role = Some(role); } else { @@ -22,7 +22,7 @@ impl Mailbox { self } - pub fn sort_order(mut self, sort_order: u32) -> Self { + pub fn sort_order(&mut self, sort_order: u32) -> &mut Self { self.sort_order = sort_order.into(); self } @@ -32,9 +32,10 @@ pub fn role_not_set(role: &Option) -> bool { matches!(role, Some(Role::None)) } -impl Mailbox { - pub fn new() -> Mailbox { +impl Create for Mailbox { + fn new(_create_id: Option) -> Self { Mailbox { + _create_id, _state: Default::default(), id: None, name: None, @@ -49,4 +50,15 @@ impl Mailbox { is_subscribed: None, } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } +} + +impl SetArguments { + pub fn on_destroy_remove_emails(&mut self, value: bool) -> &mut Self { + self.on_destroy_remove_emails = value; + self + } } diff --git a/src/push_subscription/mod.rs b/src/push_subscription/mod.rs index c863f95..ebaa533 100644 --- a/src/push_subscription/mod.rs +++ b/src/push_subscription/mod.rs @@ -9,6 +9,9 @@ use crate::{Get, Object}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PushSubscription { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, diff --git a/src/push_subscription/set.rs b/src/push_subscription/set.rs index 0b896eb..c67d907 100644 --- a/src/push_subscription/set.rs +++ b/src/push_subscription/set.rs @@ -1,42 +1,46 @@ -use crate::{core::set::from_timestamp, Object, Set}; +use crate::{ + core::set::{from_timestamp, Create}, + Object, Set, +}; use super::{Keys, PushSubscription}; impl PushSubscription { - pub fn device_client_id(mut self, device_client_id: String) -> Self { - self.device_client_id = Some(device_client_id); + pub fn device_client_id(&mut self, device_client_id: impl Into) -> &mut Self { + self.device_client_id = Some(device_client_id.into()); self } - pub fn url(mut self, url: String) -> Self { - self.url = Some(url); + pub fn url(&mut self, url: impl Into) -> &mut Self { + self.url = Some(url.into()); self } - pub fn verification_code(mut self, verification_code: String) -> Self { - self.verification_code = Some(verification_code); + pub fn verification_code(&mut self, verification_code: impl Into) -> &mut Self { + self.verification_code = Some(verification_code.into()); self } - pub fn keys(mut self, keys: Keys) -> Self { + pub fn keys(&mut self, keys: Keys) -> &mut Self { self.keys = Some(keys); self } - pub fn expires(mut self, expires: i64) -> Self { + pub fn expires(&mut self, expires: i64) -> &mut Self { self.expires = Some(from_timestamp(expires)); self } - pub fn types(mut self, types: Option>) -> Self { + pub fn types(&mut self, types: Option>) -> &mut Self { self.types = types.map(|s| s.collect()); self } } -impl PushSubscription { - pub fn new() -> PushSubscription { +impl Create for PushSubscription { + fn new(_create_id: Option) -> Self { PushSubscription { + _create_id, _state: Default::default(), id: None, device_client_id: None, @@ -47,6 +51,10 @@ impl PushSubscription { types: Vec::with_capacity(0).into(), } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } } impl Keys { diff --git a/src/vacation_response/mod.rs b/src/vacation_response/mod.rs index b5dcd72..a5d8bab 100644 --- a/src/vacation_response/mod.rs +++ b/src/vacation_response/mod.rs @@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VacationResponse { + #[serde(skip)] + _create_id: Option, + #[serde(skip)] _state: std::marker::PhantomData, diff --git a/src/vacation_response/set.rs b/src/vacation_response/set.rs index 144bca2..1d47f5d 100644 --- a/src/vacation_response/set.rs +++ b/src/vacation_response/set.rs @@ -1,42 +1,46 @@ -use crate::{core::set::from_timestamp, Set}; +use crate::{ + core::set::{from_timestamp, Create}, + Set, +}; use super::VacationResponse; impl VacationResponse { - pub fn is_enabled(mut self, is_enabled: bool) -> Self { + pub fn is_enabled(&mut self, is_enabled: bool) -> &mut Self { self.is_enabled = Some(is_enabled); self } - pub fn from_date(mut self, from_date: Option) -> Self { + pub fn from_date(&mut self, from_date: Option) -> &mut Self { self.from_date = from_date.map(from_timestamp); self } - pub fn to_date(mut self, to_date: Option) -> Self { + pub fn to_date(&mut self, to_date: Option) -> &mut Self { self.to_date = to_date.map(from_timestamp); self } - pub fn subject(mut self, subject: Option) -> Self { - self.subject = subject; + pub fn subject(&mut self, subject: Option>) -> &mut Self { + self.subject = subject.map(|s| s.into()); self } - pub fn text_body(mut self, text_body: Option) -> Self { - self.text_body = text_body; + pub fn text_body(&mut self, text_body: Option>) -> &mut Self { + self.text_body = text_body.map(|s| s.into()); self } - pub fn html_body(mut self, html_body: Option) -> Self { - self.html_body = html_body; + pub fn html_body(&mut self, html_body: Option>) -> &mut Self { + self.html_body = html_body.map(|s| s.into()); self } } -impl VacationResponse { - pub fn new() -> VacationResponse { +impl Create for VacationResponse { + fn new(_create_id: Option) -> Self { VacationResponse { + _create_id, _state: Default::default(), id: None, is_enabled: None, @@ -47,4 +51,8 @@ impl VacationResponse { html_body: "".to_string().into(), } } + + fn create_id(&self) -> Option { + self._create_id.map(|id| format!("c{}", id)) + } }