From e64f2077a86192bc61cb12cb1993d875034b2892 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 15 Oct 2022 18:10:07 +0300 Subject: [PATCH] melib/jmap: respect max_objects_in_get when fetching email --- melib/src/backends/jmap.rs | 26 +- melib/src/backends/jmap/connection.rs | 43 ++- melib/src/backends/jmap/objects.rs | 3 + melib/src/backends/jmap/objects/email.rs | 7 - melib/src/backends/jmap/objects/thread.rs | 80 ++++++ melib/src/backends/jmap/protocol.rs | 252 +++++++++--------- melib/src/backends/jmap/rfc8620.rs | 38 +-- melib/src/backends/jmap/rfc8620/argument.rs | 6 +- melib/src/backends/jmap/rfc8620/comparator.rs | 2 +- 9 files changed, 285 insertions(+), 172 deletions(-) create mode 100644 melib/src/backends/jmap/objects/thread.rs diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index 003c2e0c..21cc2857 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -72,6 +72,9 @@ macro_rules! _impl { } } +pub const JMAP_CORE_CAPABILITY: &str = "urn:ietf:params:jmap:core"; +pub const JMAP_MAIL_CAPABILITY: &str = "urn:ietf:params:jmap:mail"; + pub mod operations; use operations::*; @@ -196,6 +199,7 @@ pub struct Store { pub mailbox_state: Arc>>, pub online_status: Arc)>>, pub is_subscribed: Arc, + pub core_capabilities: Arc>, pub event_consumer: BackendEventConsumer, } @@ -330,15 +334,19 @@ impl MailBackend for JmapType { Ok(Box::pin(async_stream::try_stream! { let mut conn = connection.lock().await; conn.connect().await?; - let res = protocol::fetch( - &conn, - &store, - mailbox_hash, - ).await?; - if res.is_empty() { - return; + let batch_size: u64 = conn.store.core_capabilities.lock().unwrap().max_objects_in_get; + let mut fetch_state = protocol::EmailFetchState::Start { batch_size }; + loop { + let res = fetch_state.fetch( + &conn, + &store, + mailbox_hash, + ).await?; + if res.is_empty() { + return; + } + yield res; } - yield res; })) } @@ -889,7 +897,7 @@ impl JmapType { event_consumer, is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), collection: Collection::default(), - + core_capabilities: Arc::new(Mutex::new(rfc8620::CapabilitiesObject::default())), byte_cache: Default::default(), id_store: Default::default(), reverse_id_store: Default::default(), diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs index 56239e47..0c30ee04 100644 --- a/melib/src/backends/jmap/connection.rs +++ b/melib/src/backends/jmap/connection.rs @@ -88,19 +88,15 @@ impl JmapConnection { } Ok(s) => s, }; - if !session - .capabilities - .contains_key("urn:ietf:params:jmap:core") - { - let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::>().join(", "))); + if !session.capabilities.contains_key(JMAP_CORE_CAPABILITY) { + let err = MeliError::new(format!("Server {} did not return JMAP Core capability ({core_capability}). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::>().join(", "), core_capability=JMAP_CORE_CAPABILITY)); *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); return Err(err); } - if !session - .capabilities - .contains_key("urn:ietf:params:jmap:mail") - { - let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::>().join(", "))); + *self.store.core_capabilities.lock().unwrap() = + session.capabilities["urn:ietf:params:jmap:core"].clone(); + if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) { + let err = MeliError::new(format!("Server {} does not support JMAP Mail capability ({mail_capability}). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::>().join(", "), mail_capability=JMAP_MAIL_CAPABILITY)); *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); return Err(err); } @@ -111,7 +107,7 @@ impl JmapConnection { } pub fn mail_account_id(&self) -> Id { - self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone() + self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone() } pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> { @@ -359,4 +355,29 @@ impl JmapConnection { Ok(()) } + + pub async fn send_request(&self, request: String) -> Result { + let api_url = self.session.lock().unwrap().api_url.clone(); + let mut res = self.client.post_async(api_url.as_str(), request).await?; + + let res_text = res.text().await?; + debug!(&res_text); + let _: MethodResponse = match serde_json::from_str(&res_text) { + Err(err) => { + let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug); + *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + crate::log(err.to_string(), crate::ERROR); + crate::log( + format!( + "Tried to deserialize this server response as a MethodResponse json:\n{}", + &res_text + ), + crate::DEBUG, + ); + return Err(err); + } + Ok(s) => s, + }; + Ok(res_text) + } } diff --git a/melib/src/backends/jmap/objects.rs b/melib/src/backends/jmap/objects.rs index b4ec2ea6..cf9b715d 100644 --- a/melib/src/backends/jmap/objects.rs +++ b/melib/src/backends/jmap/objects.rs @@ -26,3 +26,6 @@ pub use email::*; mod mailbox; pub use mailbox::*; + +mod thread; +pub use thread::*; diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index b1e2f3c4..def29052 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -33,13 +33,6 @@ use std::hash::Hasher; mod import; pub use import::*; -#[derive(Debug)] -pub struct ThreadObject; - -impl Object for ThreadObject { - const NAME: &'static str = "Thread"; -} - impl Id { pub fn into_hash(&self) -> EnvelopeHash { let mut h = DefaultHasher::new(); diff --git a/melib/src/backends/jmap/objects/thread.rs b/melib/src/backends/jmap/objects/thread.rs new file mode 100644 index 00000000..bca12e62 --- /dev/null +++ b/melib/src/backends/jmap/objects/thread.rs @@ -0,0 +1,80 @@ +/* + * meli - jmap module. + * + * Copyright 2019-2022 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +use core::marker::PhantomData; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ThreadObject { + #[serde(default)] + pub id: Id, + #[serde(default)] + pub email_ids: Vec>, +} + +impl Object for ThreadObject { + const NAME: &'static str = "Thread"; +} + +impl ThreadObject { + _impl!(get email_ids, email_ids: Vec>); +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ThreadGet { + #[serde(flatten)] + pub get_call: Get, +} + +impl Method for ThreadGet { + const NAME: &'static str = "Thread/get"; +} + +impl ThreadGet { + pub const RESULT_FIELD_THREAD_IDS: ResultField = + ResultField:: { + field: "/list/*/threadId", + _ph: PhantomData, + }; + + pub fn new(get_call: Get) -> Self { + ThreadGet { get_call } + } +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ThreadChanges { + #[serde(flatten)] + pub changes_call: Changes, +} + +impl Method for ThreadChanges { + const NAME: &'static str = "Thread/changes"; +} + +impl ThreadChanges { + pub fn new(changes_call: Changes) -> Self { + ThreadChanges { changes_call } + } +} diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 7eb7e640..08ba31da 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -27,7 +27,7 @@ use std::convert::{TryFrom, TryInto}; pub type UtcDate = String; -use super::rfc8620::Object; +use super::rfc8620::{Object, State}; macro_rules! get_request_no { ($lock:expr) => {{ @@ -86,30 +86,16 @@ pub struct JsonResponse<'a> { pub async fn get_mailboxes(conn: &JmapConnection) -> Result> { let seq = get_request_no!(conn.request_no); - let api_url = conn.session.lock().unwrap().api_url.clone(); - let mut res = conn - .client - .post_async( - api_url.as_str(), - serde_json::to_string(&json!({ - "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], - "methodCalls": [["Mailbox/get", { - "accountId": conn.mail_account_id() - }, - format!("#m{}",seq).as_str()]], - }))?, - ) + let res_text = conn + .send_request(serde_json::to_string(&json!({ + "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], + "methodCalls": [["Mailbox/get", { + "accountId": conn.mail_account_id() + }, + format!("#m{}",seq).as_str()]], + }))?) .await?; - - let res_text = res.text().await?; - let mut v: MethodResponse = match serde_json::from_str(&res_text) { - Err(err) => { - let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug); - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); - return Err(err); - } - Ok(s) => s, - }; + let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap(); *conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(())); let m = GetResponse::::try_from(v.method_responses.remove(0))?; let GetResponse:: { @@ -249,109 +235,131 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result Result> { - let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone(); - let email_query_call: EmailQuery = EmailQuery::new( - Query::new() - .account_id(conn.mail_account_id().clone()) - .filter(Some(Filter::Condition( - EmailFilterCondition::new() - .in_mailbox(Some(mailbox_id)) - .into(), - ))) - .position(0), - ) - .collapse_threads(false); +#[derive(Copy, Clone)] +pub enum EmailFetchState { + Start { batch_size: u64 }, + Ongoing { position: u64, batch_size: u64 }, +} - let mut req = Request::new(conn.request_no.clone()); - let prev_seq = req.add_call(&email_query_call); - - let email_call: EmailGet = EmailGet::new( - Get::new() - .ids(Some(JmapArgument::reference( - prev_seq, - EmailQuery::RESULT_FIELD_IDS, - ))) - .account_id(conn.mail_account_id().clone()), - ); - - req.add_call(&email_call); - - let api_url = conn.session.lock().unwrap().api_url.clone(); - let mut res = conn - .client - .post_async(api_url.as_str(), serde_json::to_string(&req)?) - .await?; - - let res_text = res.text().await?; - - let mut v: MethodResponse = match serde_json::from_str(&res_text) { - Err(err) => { - let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug); - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); - return Err(err); +impl EmailFetchState { + pub async fn must_update_state( + &mut self, + conn: &JmapConnection, + mailbox_hash: MailboxHash, + state: State, + ) -> Result { + { + let (is_empty, is_equal) = { + let mailboxes_lck = conn.store.mailboxes.read().unwrap(); + mailboxes_lck + .get(&mailbox_hash) + .map(|mbox| { + let current_state_lck = mbox.email_state.lock().unwrap(); + ( + current_state_lck.is_none(), + current_state_lck.as_ref() != Some(&state), + ) + }) + .unwrap_or((true, true)) + }; + if is_empty { + let mut mailboxes_lck = conn.store.mailboxes.write().unwrap(); + debug!("{:?}: inserting state {}", EmailObject::NAME, &state); + mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { + *mbox.email_state.lock().unwrap() = Some(state); + }); + } else if !is_equal { + conn.email_changes(mailbox_hash).await?; + } + Ok(is_empty || !is_equal) } - Ok(s) => s, - }; - let e = GetResponse::::try_from(v.method_responses.pop().unwrap())?; - let query_response = QueryResponse::::try_from(v.method_responses.pop().unwrap())?; - store - .mailboxes - .write() - .unwrap() - .entry(mailbox_hash) - .and_modify(|mbox| { - *mbox.email_query_state.lock().unwrap() = Some(query_response.query_state); - }); - let GetResponse:: { list, state, .. } = e; - { - let (is_empty, is_equal) = { - let mailboxes_lck = conn.store.mailboxes.read().unwrap(); - mailboxes_lck - .get(&mailbox_hash) - .map(|mbox| { - let current_state_lck = mbox.email_state.lock().unwrap(); - ( - current_state_lck.is_none(), - current_state_lck.as_ref() != Some(&state), + } + + pub async fn fetch( + &mut self, + conn: &JmapConnection, + store: &Store, + mailbox_hash: MailboxHash, + ) -> Result> { + loop { + match *self { + EmailFetchState::Start { batch_size } => { + *self = EmailFetchState::Ongoing { + position: 0, + batch_size, + }; + continue; + } + EmailFetchState::Ongoing { + mut position, + batch_size, + } => { + let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone(); + let email_query_call: EmailQuery = EmailQuery::new( + Query::new() + .account_id(conn.mail_account_id().clone()) + .filter(Some(Filter::Condition( + EmailFilterCondition::new() + .in_mailbox(Some(mailbox_id)) + .into(), + ))) + .position(position) + .limit(Some(batch_size)), ) - }) - .unwrap_or((true, true)) - }; - if is_empty { - let mut mailboxes_lck = conn.store.mailboxes.write().unwrap(); - debug!("{:?}: inserting state {}", EmailObject::NAME, &state); - mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { - *mbox.email_state.lock().unwrap() = Some(state); - }); - } else if !is_equal { - conn.email_changes(mailbox_hash).await?; + .collapse_threads(false); + + let mut req = Request::new(conn.request_no.clone()); + let prev_seq = req.add_call(&email_query_call); + + let email_call: EmailGet = EmailGet::new( + Get::new() + .ids(Some(JmapArgument::reference( + prev_seq, + EmailQuery::RESULT_FIELD_IDS, + ))) + .account_id(conn.mail_account_id().clone()), + ); + + let _prev_seq = req.add_call(&email_call); + let res_text = conn.send_request(serde_json::to_string(&req)?).await?; + let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap(); + let e = + GetResponse::::try_from(v.method_responses.pop().unwrap())?; + let GetResponse:: { list, state, .. } = e; + + if self.must_update_state(conn, mailbox_hash, state).await? { + *self = EmailFetchState::Start { batch_size }; + continue; + } + let mut total = BTreeSet::default(); + let mut unread = BTreeSet::default(); + let mut ret = Vec::with_capacity(list.len()); + for obj in list { + let env = store.add_envelope(obj); + total.insert(env.hash()); + if !env.is_seen() { + unread.insert(env.hash()); + } + ret.push(env); + } + let mut mailboxes_lck = store.mailboxes.write().unwrap(); + mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { + mbox.total_emails.lock().unwrap().insert_existing_set(total); + mbox.unread_emails + .lock() + .unwrap() + .insert_existing_set(unread); + }); + position += batch_size; + *self = EmailFetchState::Ongoing { + position, + batch_size, + }; + return Ok(ret); + } + } } } - let mut total = BTreeSet::default(); - let mut unread = BTreeSet::default(); - let mut ret = Vec::with_capacity(list.len()); - for obj in list { - let env = store.add_envelope(obj); - total.insert(env.hash()); - if !env.is_seen() { - unread.insert(env.hash()); - } - ret.push(env); - } - let mut mailboxes_lck = store.mailboxes.write().unwrap(); - mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { - mbox.total_emails.lock().unwrap().insert_existing_set(total); - mbox.unread_emails - .lock() - .unwrap() - .insert_existing_set(unread); - }); - Ok(ret) } pub fn keywords_to_flags(keywords: Vec) -> (Flag, Vec) { diff --git a/melib/src/backends/jmap/rfc8620.rs b/melib/src/backends/jmap/rfc8620.rs index 5142435c..e7b6e462 100644 --- a/melib/src/backends/jmap/rfc8620.rs +++ b/melib/src/backends/jmap/rfc8620.rs @@ -202,7 +202,7 @@ impl State { } } -#[derive(Deserialize, Serialize, Debug, Default)] +#[derive(Deserialize, Serialize, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct JmapSession { pub capabilities: HashMap, @@ -223,7 +223,7 @@ impl Object for JmapSession { const NAME: &'static str = "Session"; } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Default, Debug)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesObject { #[serde(default)] @@ -244,7 +244,7 @@ pub struct CapabilitiesObject { pub collation_algorithms: Vec, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Account { pub name: String, @@ -259,7 +259,7 @@ impl Object for Account { const NAME: &'static str = "Account"; } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] pub struct BlobObject; impl Object for BlobObject { @@ -276,7 +276,7 @@ impl Object for BlobObject { /// /// The id of the account to use. /// -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Get where @@ -389,7 +389,7 @@ impl Serialize for Get { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct MethodResponse<'a> { #[serde(borrow)] @@ -400,7 +400,7 @@ pub struct MethodResponse<'a> { pub session_state: State, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct GetResponse { pub account_id: Id, @@ -427,7 +427,7 @@ impl GetResponse { _impl!(get_mut not_found_mut, not_found: Vec>); } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Copy, Debug)] #[serde(rename_all = "camelCase")] enum JmapError { RequestTooLarge, @@ -435,7 +435,7 @@ enum JmapError { InvalidResultReference, } -#[derive(Serialize, Debug)] +#[derive(Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Query, OBJ: Object> where @@ -499,7 +499,7 @@ pub fn bool_true() -> bool { true } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryResponse { pub account_id: Id, @@ -580,7 +580,7 @@ impl, OBJ: Object> ResultField { /// positive integer greater than 0. If a value outside of this range /// is given, the server MUST re /// -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] /* ch-ch-ch-ch-ch-Changes */ pub struct Changes @@ -636,7 +636,7 @@ where ); } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct ChangesResponse { pub account_id: Id, @@ -678,7 +678,7 @@ impl ChangesResponse { ///and dependencies that may exist if doing multiple operations at once ///(for example, to ensure there is always a minimum number of a certain ///record type). -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Set where @@ -788,7 +788,7 @@ where _impl!(update: Option, Value>>); } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct SetResponse { ///o accountId: "Id" @@ -859,7 +859,7 @@ impl std::convert::TryFrom<&RawValue> for SetRes } } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] #[serde(tag = "type", content = "description")] pub enum SetError { @@ -992,7 +992,7 @@ pub fn upload_request_format(upload_url: &str, account_id: &Id) -> Stri ret } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct UploadResponse { ///o accountId: "Id" @@ -1024,7 +1024,7 @@ pub struct UploadResponse { /// The "Foo/queryChanges" method allows a client to efficiently update /// the state of a cached query to match the new state on the server. It /// takes the following arguments: -#[derive(Serialize, Debug)] +#[derive(Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryChanges, OBJ: Object> where @@ -1093,7 +1093,7 @@ where _impl!(calculate_total: bool); } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryChangesResponse { /// The id of the account used for the call. @@ -1170,7 +1170,7 @@ pub struct QueryChangesResponse { pub added: Vec>, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct AddedItem { pub id: Id, diff --git a/melib/src/backends/jmap/rfc8620/argument.rs b/melib/src/backends/jmap/rfc8620/argument.rs index 1410d0b8..a21bd03d 100644 --- a/melib/src/backends/jmap/rfc8620/argument.rs +++ b/melib/src/backends/jmap/rfc8620/argument.rs @@ -23,9 +23,9 @@ use crate::backends::jmap::protocol::Method; use crate::backends::jmap::rfc8620::Object; use crate::backends::jmap::rfc8620::ResultField; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] -pub enum JmapArgument { +pub enum JmapArgument { Value(T), ResultReference { result_of: String, @@ -34,7 +34,7 @@ pub enum JmapArgument { }, } -impl JmapArgument { +impl JmapArgument { pub fn value(v: T) -> Self { JmapArgument::Value(v) } diff --git a/melib/src/backends/jmap/rfc8620/comparator.rs b/melib/src/backends/jmap/rfc8620/comparator.rs index f5170f3d..ef717e90 100644 --- a/melib/src/backends/jmap/rfc8620/comparator.rs +++ b/melib/src/backends/jmap/rfc8620/comparator.rs @@ -21,7 +21,7 @@ use super::*; -#[derive(Serialize, Debug)] +#[derive(Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Comparator { property: String,