diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index 31e58ab3f..cf1f7c3a9 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -191,15 +191,16 @@ macro_rules! get_conf_val { pub struct Store { pub account_name: Arc, pub account_hash: AccountHash, - pub account_id: Arc>, + pub account_id: Arc>>, pub byte_cache: Arc>>, - pub id_store: Arc>>, - pub reverse_id_store: Arc>>, - pub blob_id_store: Arc>>, + pub id_store: Arc>>>, + pub reverse_id_store: Arc, EnvelopeHash>>>, + pub blob_id_store: Arc>>>, pub tag_index: Arc>>, pub mailboxes: Arc>>, pub mailboxes_index: Arc>>>, - pub object_set_states: Arc>>, + pub email_state: Arc>>, + pub mailbox_state: Arc>>, pub online_status: Arc)>>, pub is_subscribed: Arc, pub event_consumer: BackendEventConsumer, @@ -275,7 +276,7 @@ impl Store { pub fn remove_envelope( &self, - obj_id: Id, + obj_id: Id, ) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> { let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?; self.id_store.lock().unwrap().remove(&env_hash); @@ -413,7 +414,7 @@ impl MailBackend for JmapType { ) .await?; - let mailbox_id: String = { + let mailbox_id: Id = { let mailboxes_lck = store.mailboxes.read().unwrap(); if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) { mailbox.id.clone() @@ -428,7 +429,7 @@ impl MailBackend for JmapType { let upload_response: UploadResponse = serde_json::from_str(&res_text)?; let mut req = Request::new(conn.request_no.clone()); - let creation_id = "1".to_string(); + let creation_id: Id = "1".to_string().into(); let mut email_imports = HashMap::default(); let mut mailbox_ids = HashMap::default(); mailbox_ids.insert(mailbox_id, true); @@ -440,7 +441,7 @@ impl MailBackend for JmapType { ); let import_call: ImportCall = ImportCall::new() - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .emails(email_imports); req.add_call(&import_call); @@ -508,7 +509,7 @@ impl MailBackend for JmapType { conn.connect().await?; let email_call: EmailQuery = EmailQuery::new( Query::new() - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .filter(Some(filter)) .position(0), ) @@ -527,14 +528,7 @@ impl MailBackend for JmapType { *store.online_status.lock().await = (std::time::Instant::now(), Ok(())); let m = QueryResponse::::try_from(v.method_responses.remove(0))?; let QueryResponse:: { ids, .. } = m; - let ret = ids - .into_iter() - .map(|id| { - let mut h = DefaultHasher::new(); - h.write(id.as_bytes()); - h.finish() - }) - .collect(); + let ret = ids.into_iter().map(|id| id.into_hash()).collect(); Ok(ret) })) } @@ -584,9 +578,9 @@ impl MailBackend for JmapType { mailboxes_lck[&destination_mailbox_hash].id.clone(), ) }; - let mut update_map: HashMap = HashMap::default(); - let mut ids: Vec = Vec::with_capacity(env_hashes.rest.len() + 1); - let mut id_map: HashMap = HashMap::default(); + let mut update_map: HashMap, Value> = HashMap::default(); + let mut ids: Vec> = Vec::with_capacity(env_hashes.rest.len() + 1); + let mut id_map: HashMap, EnvelopeHash> = HashMap::default(); let mut update_keywords: HashMap = HashMap::default(); update_keywords.insert( format!("mailboxIds/{}", &destination_mailbox_id), @@ -594,7 +588,7 @@ impl MailBackend for JmapType { ); if move_ { update_keywords.insert( - format!("mailboxIds/{}", &source_mailbox_hash), + format!("mailboxIds/{}", &source_mailbox_id), serde_json::json!(null), ); } @@ -611,7 +605,7 @@ impl MailBackend for JmapType { let email_set_call: EmailSet = EmailSet::new( Set::::new() - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .update(Some(update_map)), ); @@ -653,9 +647,9 @@ impl MailBackend for JmapType { let connection = self.connection.clone(); Ok(Box::pin(async move { let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone(); - let mut update_map: HashMap = HashMap::default(); - let mut ids: Vec = Vec::with_capacity(env_hashes.rest.len() + 1); - let mut id_map: HashMap = HashMap::default(); + let mut update_map: HashMap, Value> = HashMap::default(); + let mut ids: Vec> = Vec::with_capacity(env_hashes.rest.len() + 1); + let mut id_map: HashMap, EnvelopeHash> = HashMap::default(); let mut update_keywords: HashMap = HashMap::default(); for (flag, value) in flags.iter() { match flag { @@ -701,11 +695,11 @@ impl MailBackend for JmapType { } } } - let conn = connection.lock().await; + let mut conn = connection.lock().await; let email_set_call: EmailSet = EmailSet::new( Set::::new() - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .update(Some(update_map)), ); @@ -714,7 +708,7 @@ impl MailBackend for JmapType { let email_call: EmailGet = EmailGet::new( Get::new() .ids(Some(JmapArgument::Value(ids))) - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .properties(Some(vec!["keywords".to_string()])), ); @@ -759,21 +753,15 @@ impl MailBackend for JmapType { let e = GetResponse::::try_from(v.method_responses.pop().unwrap())?; let GetResponse:: { list, state, .. } = e; { - let c = store - .object_set_states - .lock() - .unwrap() - .get(&EmailObject::NAME) - .map(|prev_state| *prev_state == state); - if let Some(false) = c { - conn.email_changes().await?; - } else { + let (is_empty, is_equal) = { + let current_state_lck = conn.store.email_state.lock().unwrap(); + (current_state_lck.is_empty(), *current_state_lck != state) + }; + if is_empty { debug!("{:?}: inserting state {}", EmailObject::NAME, &state); - store - .object_set_states - .lock() - .unwrap() - .insert(EmailObject::NAME, state); + *conn.store.email_state.lock().unwrap() = state; + } else if !is_equal { + conn.email_changes().await?; } } debug!(&list); @@ -813,7 +801,7 @@ impl JmapType { let store = Arc::new(Store { account_name: Arc::new(s.name.clone()), account_hash, - account_id: Arc::new(Mutex::new(String::new())), + account_id: Arc::new(Mutex::new(Id::new())), online_status, event_consumer, is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), @@ -825,7 +813,8 @@ impl JmapType { tag_index: Default::default(), mailboxes: Default::default(), mailboxes_index: Default::default(), - object_set_states: Default::default(), + email_state: Default::default(), + mailbox_state: Default::default(), }); Ok(Box::new(JmapType { diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs index af46bba64..97ad15c66 100644 --- a/melib/src/backends/jmap/connection.rs +++ b/melib/src/backends/jmap/connection.rs @@ -100,7 +100,7 @@ impl JmapConnection { Ok(()) } - pub fn mail_account_id(&self) -> &Id { + pub fn mail_account_id(&self) -> &Id { &self.session.primary_accounts["urn:ietf:params:jmap:mail"] } @@ -109,22 +109,16 @@ impl JmapConnection { } pub async fn email_changes(&self) -> Result<()> { - let mut current_state: String = { - let object_set_states_lck = self.store.object_set_states.lock().unwrap(); - let v = if let Some(prev_state) = debug!(object_set_states_lck.get(&EmailObject::NAME)) - { - prev_state.clone() - } else { - return Ok(()); - }; - drop(object_set_states_lck); - v - }; + let mut current_state: State = self.store.email_state.lock().unwrap().clone(); + if current_state.is_empty() { + debug!("{:?}: has no saved state", EmailObject::NAME); + return Ok(()); + } loop { let email_changes_call: EmailChanges = EmailChanges::new( Changes::::new() - .account_id(self.mail_account_id().to_string()) + .account_id(self.mail_account_id().clone()) .since_state(current_state.clone()), ); @@ -136,7 +130,7 @@ impl JmapConnection { prev_seq, ResultField::::new("created"), ))) - .account_id(self.mail_account_id().to_string()), + .account_id(self.mail_account_id().clone()), ); req.add_call(&email_get_call); @@ -153,6 +147,12 @@ impl JmapConnection { GetResponse::::try_from(v.method_responses.pop().unwrap())?; debug!(&get_response); let GetResponse:: { list, .. } = get_response; + let changes_response = + ChangesResponse::::try_from(v.method_responses.pop().unwrap())?; + if changes_response.new_state == current_state { + return Ok(()); + } + let mut mailbox_hashes: Vec> = Vec::with_capacity(list.len()); for envobj in &list { @@ -189,9 +189,6 @@ impl JmapConnection { } } - let changes_response = - ChangesResponse::::try_from(v.method_responses.pop().unwrap())?; - let ChangesResponse:: { account_id: _, new_state, @@ -218,11 +215,7 @@ impl JmapConnection { if has_more_changes { current_state = new_state; } else { - self.store - .object_set_states - .lock() - .unwrap() - .insert(EmailObject::NAME, new_state); + *self.store.email_state.lock().unwrap() = new_state; break; } } diff --git a/melib/src/backends/jmap/mailbox.rs b/melib/src/backends/jmap/mailbox.rs index cf1987a46..33e87637a 100644 --- a/melib/src/backends/jmap/mailbox.rs +++ b/melib/src/backends/jmap/mailbox.rs @@ -29,10 +29,11 @@ pub struct JmapMailbox { pub path: String, pub hash: MailboxHash, pub children: Vec, - pub id: String, + pub id: Id, pub is_subscribed: bool, pub my_rights: JmapRights, - pub parent_id: Option, + pub parent_id: Option>, + pub parent_hash: Option, pub role: Option, pub sort_order: u64, pub total_emails: Arc>, @@ -66,7 +67,7 @@ impl BackendMailbox for JmapMailbox { } fn parent(&self) -> Option { - None + self.parent_hash } fn permissions(&self) -> MailboxPermissions { diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index 415b62bc3..6e2feda4d 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -32,6 +32,21 @@ 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(); + h.write(self.inner.as_bytes()); + h.finish() + } +} + // 4.1.1. // Metadata // These properties represent metadata about the message in the mail @@ -133,11 +148,11 @@ pub use import::*; #[serde(rename_all = "camelCase")] pub struct EmailObject { #[serde(default)] - pub id: Id, + pub id: Id, #[serde(default)] - pub blob_id: String, + pub blob_id: Id, #[serde(default)] - pub mailbox_ids: HashMap, + pub mailbox_ids: HashMap, bool>, #[serde(default)] pub size: u64, #[serde(default)] @@ -163,7 +178,7 @@ pub struct EmailObject { #[serde(default)] pub keywords: HashMap, #[serde(default)] - pub attached_emails: Option, + pub attached_emails: Option>, #[serde(default)] pub attachments: Vec, #[serde(default)] @@ -182,7 +197,7 @@ pub struct EmailObject { #[serde(default)] pub text_body: Vec, #[serde(default)] - pub thread_id: Id, + pub thread_id: Id, #[serde(flatten)] pub extra: HashMap, } @@ -313,9 +328,7 @@ impl std::convert::From for crate::Envelope { } } - let mut h = DefaultHasher::new(); - h.write(t.id.as_bytes()); - env.set_hash(h.finish()); + env.set_hash(t.id.into_hash()); env } } @@ -323,7 +336,7 @@ impl std::convert::From for crate::Envelope { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct HtmlBody { - pub blob_id: Id, + pub blob_id: Id, #[serde(default)] pub charset: String, #[serde(default)] @@ -350,7 +363,7 @@ pub struct HtmlBody { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct TextBody { - pub blob_id: Id, + pub blob_id: Id, #[serde(default)] pub charset: String, #[serde(default)] @@ -381,12 +394,12 @@ impl Object for EmailObject { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct EmailQueryResponse { - pub account_id: Id, + pub account_id: Id, pub can_calculate_changes: bool, pub collapse_threads: bool, // FIXME pub filter: String, - pub ids: Vec, + pub ids: Vec>, pub position: u64, pub query_state: String, pub sort: Option, @@ -469,9 +482,9 @@ impl EmailGet { #[serde(rename_all = "camelCase")] pub struct EmailFilterCondition { #[serde(skip_serializing_if = "Option::is_none")] - pub in_mailbox: Option, + pub in_mailbox: Option>, #[serde(skip_serializing_if = "Vec::is_empty")] - pub in_mailbox_other_than: Vec, + pub in_mailbox_other_than: Vec>, #[serde(skip_serializing_if = "String::is_empty")] pub before: UtcDate, #[serde(skip_serializing_if = "String::is_empty")] @@ -517,8 +530,8 @@ impl EmailFilterCondition { Self::default() } - _impl!(in_mailbox: Option); - _impl!(in_mailbox_other_than: Vec); + _impl!(in_mailbox: Option>); + _impl!(in_mailbox_other_than: Vec>); _impl!(before: UtcDate); _impl!(after: UtcDate); _impl!(min_size: Option); @@ -728,7 +741,7 @@ fn test_jmap_query() { let mut r = Filter::Condition( EmailFilterCondition::new() - .in_mailbox(Some(mailbox_id)) + .in_mailbox(Some(mailbox_id.into())) .into(), ); r &= f; @@ -737,7 +750,7 @@ fn test_jmap_query() { let email_call: EmailQuery = EmailQuery::new( Query::new() - .account_id("account_id".to_string()) + .account_id("account_id".to_string().into()) .filter(Some(filter)) .position(0), ) diff --git a/melib/src/backends/jmap/objects/email/import.rs b/melib/src/backends/jmap/objects/email/import.rs index 5fd0895a3..638c58c3f 100644 --- a/melib/src/backends/jmap/objects/email/import.rs +++ b/melib/src/backends/jmap/objects/email/import.rs @@ -37,7 +37,7 @@ use serde_json::value::RawValue; pub struct ImportCall { ///accountId: "Id" ///The id of the account to use. - pub account_id: String, + pub account_id: Id, ///ifInState: "String|null" ///This is a state string as returned by the "Email/get" method. If ///supplied, the string must match the current state of the account @@ -45,10 +45,10 @@ pub struct ImportCall { ///and a "stateMismatch" error returned. If null, any changes will ///be applied to the current state. #[serde(skip_serializing_if = "Option::is_none")] - pub if_in_state: Option, + pub if_in_state: Option>, ///o emails: "Id[EmailImport]" ///A map of creation id (client specified) to EmailImport objects. - pub emails: HashMap, + pub emails: HashMap, EmailImport>, } #[derive(Deserialize, Serialize, Debug)] @@ -56,11 +56,11 @@ pub struct ImportCall { pub struct EmailImport { ///o blobId: "Id" ///The id of the blob containing the raw message [RFC5322]. - pub blob_id: String, + pub blob_id: Id, ///o mailboxIds: "Id[Boolean]" ///The ids of the Mailboxes to assign this Email to. At least one ///Mailbox MUST be given. - pub mailbox_ids: HashMap, + pub mailbox_ids: HashMap, bool>, ///o keywords: "String[Boolean]" (default: {}) ///The keywords to apply to the Email. pub keywords: HashMap, @@ -74,7 +74,7 @@ pub struct EmailImport { impl ImportCall { pub fn new() -> Self { Self { - account_id: String::new(), + account_id: Id::new(), if_in_state: None, emails: HashMap::default(), } @@ -85,10 +85,10 @@ impl ImportCall { /// /// The id of the account to use. /// - account_id: String + account_id: Id ); - _impl!(if_in_state: Option); - _impl!(emails: HashMap); + _impl!(if_in_state: Option>); + _impl!(emails: HashMap, EmailImport>); } impl Method for ImportCall { @@ -98,15 +98,15 @@ impl Method for ImportCall { impl EmailImport { pub fn new() -> Self { Self { - blob_id: String::new(), + blob_id: Id::new(), mailbox_ids: HashMap::default(), keywords: HashMap::default(), received_at: None, } } - _impl!(blob_id: String); - _impl!(mailbox_ids: HashMap); + _impl!(blob_id: Id); + _impl!(mailbox_ids: HashMap, bool>); _impl!(keywords: HashMap); _impl!(received_at: Option); } @@ -126,7 +126,7 @@ pub enum ImportError { ///the SetError object with the id of the existing Email. If duplicates ///are allowed, the newly created Email object MUST have a separate id ///and independent mutable properties to the existing object. - existing_id: Id, + existing_id: Id, }, ///If the "blobId", "mailboxIds", or "keywords" properties are invalid ///(e.g., missing, wrong type, id not found), the server MUST reject the @@ -155,30 +155,30 @@ pub enum ImportError { pub struct ImportResponse { ///o accountId: "Id" ///The id of the account used for this call. - pub account_id: Id, + pub account_id: Id, ///o oldState: "String|null" ///The state string that would have been returned by "Email/get" on ///this account before making the requested changes, or null if the ///server doesn't know what the previous state string was. - pub old_state: Option, + pub old_state: Option>, ///o newState: "String" ///The state string that will now be returned by "Email/get" on this ///account. - pub new_state: Option, + pub new_state: Option>, ///o created: "Id[Email]|null" ///A map of the creation id to an object containing the "id", ///"blobId", "threadId", and "size" properties for each successfully ///imported Email, or null if none. - pub created: HashMap, + pub created: HashMap, ImportEmailResult>, ///o notCreated: "Id[SetError]|null" ///A map of the creation id to a SetError object for each Email that ///failed to be created, or null if all successful. The possible ///errors are defined above. - pub not_created: HashMap, + pub not_created: HashMap, ImportError>, } impl std::convert::TryFrom<&RawValue> for ImportResponse { @@ -193,8 +193,8 @@ impl std::convert::TryFrom<&RawValue> for ImportResponse { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ImportEmailResult { - pub id: Id, - pub blob_id: Id, - pub thread_id: Id, + pub id: Id, + pub blob_id: Id, + pub thread_id: Id, pub size: usize, } diff --git a/melib/src/backends/jmap/objects/mailbox.rs b/melib/src/backends/jmap/objects/mailbox.rs index 1c41e392b..9a25b4ec6 100644 --- a/melib/src/backends/jmap/objects/mailbox.rs +++ b/melib/src/backends/jmap/objects/mailbox.rs @@ -21,14 +21,22 @@ use super::*; +impl Id { + pub fn into_hash(&self) -> MailboxHash { + let mut h = DefaultHasher::new(); + h.write(self.inner.as_bytes()); + h.finish() + } +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MailboxObject { - pub id: String, + pub id: Id, pub is_subscribed: bool, pub my_rights: JmapRights, pub name: String, - pub parent_id: Option, + pub parent_id: Option>, pub role: Option, pub sort_order: u64, pub total_emails: u64, diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index cf6b3d0cc..382a56e27 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -25,7 +25,6 @@ use serde::Serialize; use serde_json::{json, Value}; use std::convert::TryFrom; -pub type Id = String; pub type UtcDate = String; use super::rfc8620::Object; @@ -125,7 +124,8 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result Result Result Result> { +pub async fn get_message_list( + conn: &JmapConnection, + mailbox: &JmapMailbox, +) -> Result>> { let email_call: EmailQuery = EmailQuery::new( Query::new() - .account_id(conn.mail_account_id().to_string()) + .account_id(conn.mail_account_id().clone()) .filter(Some(Filter::Condition( EmailFilterCondition::new() .in_mailbox(Some(mailbox.id.clone())) @@ -213,7 +217,7 @@ pub async fn fetch( 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().to_string()) + .account_id(conn.mail_account_id().clone()) .filter(Some(Filter::Condition( EmailFilterCondition::new() .in_mailbox(Some(mailbox_id)) @@ -232,7 +236,7 @@ pub async fn fetch( prev_seq, EmailQuery::RESULT_FIELD_IDS, ))) - .account_id(conn.mail_account_id().to_string()), + .account_id(conn.mail_account_id().clone()), ); req.add_call(&email_call); @@ -248,22 +252,15 @@ pub async fn fetch( let e = GetResponse::::try_from(v.method_responses.pop().unwrap())?; let GetResponse:: { list, state, .. } = e; { - let v = conn - .store - .object_set_states - .lock() - .unwrap() - .get(&EmailObject::NAME) - .map(|prev_state| *prev_state == state); - if let Some(false) = v { - conn.email_changes().await?; - } else { + let (is_empty, is_equal) = { + let current_state_lck = conn.store.email_state.lock().unwrap(); + (current_state_lck.is_empty(), *current_state_lck != state) + }; + if is_empty { debug!("{:?}: inserting state {}", EmailObject::NAME, &state); - conn.store - .object_set_states - .lock() - .unwrap() - .insert(EmailObject::NAME, state); + *conn.store.email_state.lock().unwrap() = state; + } else if !is_equal { + conn.email_changes().await?; } } let mut ret = Vec::with_capacity(list.len()); diff --git a/melib/src/backends/jmap/rfc8620.rs b/melib/src/backends/jmap/rfc8620.rs index 863221ee3..d49bb5934 100644 --- a/melib/src/backends/jmap/rfc8620.rs +++ b/melib/src/backends/jmap/rfc8620.rs @@ -19,12 +19,12 @@ * along with meli. If not, see . */ -use super::Id; use crate::email::parser::BytesExt; use core::marker::PhantomData; use serde::de::DeserializeOwned; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_json::{value::RawValue, Value}; +use std::hash::{Hash, Hasher}; mod filters; pub use filters::*; @@ -39,23 +39,189 @@ pub trait Object { const NAME: &'static str; } +#[derive(Deserialize, Serialize)] +#[serde(transparent)] +pub struct Id { + pub inner: String, + #[serde(skip)] + pub _ph: PhantomData OBJ>, +} + +impl core::fmt::Debug for Id { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple(&format!("Id<{}>", OBJ::NAME)) + .field(&self.inner) + .finish() + } +} + +impl core::fmt::Debug for Id { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple("Id").field(&self.inner).finish() + } +} + +//, Hash, Eq, PartialEq, Default)] +impl Clone for Id { + fn clone(&self) -> Self { + Id { + inner: self.inner.clone(), + _ph: PhantomData, + } + } +} + +impl std::cmp::Eq for Id {} + +impl std::cmp::PartialEq for Id { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Hash for Id { + fn hash(&self, state: &mut H) { + self.inner.hash(state); + } +} + +impl Default for Id { + fn default() -> Self { + Self::new() + } +} + +impl From for Id { + fn from(inner: String) -> Self { + Id { + inner, + _ph: PhantomData, + } + } +} + +impl core::fmt::Display for Id { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(&self.inner, fmt) + } +} + +impl Id { + pub fn new() -> Self { + Self { + inner: String::new(), + _ph: PhantomData, + } + } + + pub fn as_str(&self) -> &str { + self.inner.as_str() + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(transparent)] +pub struct State { + pub inner: String, + #[serde(skip)] + pub _ph: PhantomData OBJ>, +} + +//, Hash, Eq, PartialEq, Default)] +impl Clone for State { + fn clone(&self) -> Self { + State { + inner: self.inner.clone(), + _ph: PhantomData, + } + } +} + +impl std::cmp::Eq for State {} + +impl std::cmp::PartialEq for State { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Hash for State { + fn hash(&self, state: &mut H) { + self.inner.hash(state); + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +impl From for State { + fn from(inner: String) -> Self { + State { + inner, + _ph: PhantomData, + } + } +} + +impl core::fmt::Display for State { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(&self.inner, fmt) + } +} + +impl State { + pub fn new() -> Self { + Self { + inner: String::new(), + _ph: PhantomData, + } + } + + pub fn as_str(&self) -> &str { + self.inner.as_str() + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + #[derive(Deserialize, Serialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct JmapSession { pub capabilities: HashMap, - pub accounts: HashMap, - pub primary_accounts: HashMap, + pub accounts: HashMap, Account>, + pub primary_accounts: HashMap>, pub username: String, pub api_url: String, pub download_url: String, pub upload_url: String, pub event_source_url: String, - pub state: String, + pub state: State, #[serde(flatten)] pub extra_properties: HashMap, } +impl Object for JmapSession { + const NAME: &'static str = "Session"; +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesObject { @@ -88,6 +254,17 @@ pub struct Account { extra_properties: HashMap, } +impl Object for Account { + const NAME: &'static str = "Account"; +} + +#[derive(Debug)] +pub struct BlobObject; + +impl Object for BlobObject { + const NAME: &'static str = "Blob"; +} + /// #`get` /// /// Objects of type `Foo` are fetched via a call to `Foo/get`. @@ -104,11 +281,10 @@ pub struct Get where OBJ: std::fmt::Debug + Serialize, { - #[serde(skip_serializing_if = "String::is_empty")] - pub account_id: String, + pub account_id: Id, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] - pub ids: Option>>, + pub ids: Option>>>, #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>, #[serde(skip)] @@ -121,7 +297,7 @@ where { pub fn new() -> Self { Self { - account_id: String::new(), + account_id: Id::new(), ids: None, properties: None, _ph: PhantomData, @@ -132,7 +308,7 @@ where /// /// The id of the account to use. /// - account_id: String + account_id: Id ); _impl!( /// - ids: `Option>>` @@ -142,7 +318,7 @@ where /// type and the number of records does not exceed the /// "max_objects_in_get" limit. /// - ids: Option>> + ids: Option>>> ); _impl!( /// - properties: Option> @@ -218,20 +394,19 @@ pub struct MethodResponse<'a> { #[serde(borrow)] pub method_responses: Vec<&'a RawValue>, #[serde(default)] - pub created_ids: HashMap, + pub created_ids: HashMap, Id>, #[serde(default)] - pub session_state: String, + pub session_state: State, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct GetResponse { - #[serde(skip_serializing_if = "String::is_empty")] - pub account_id: String, - #[serde(default)] - pub state: String, + pub account_id: Id, + #[serde(default = "State::default")] + pub state: State, pub list: Vec, - pub not_found: Vec, + pub not_found: Vec>, } impl std::convert::TryFrom<&RawValue> for GetResponse { @@ -244,10 +419,10 @@ impl std::convert::TryFrom<&RawValue> for GetRes } impl GetResponse { - _impl!(get_mut account_id_mut, account_id: String); - _impl!(get_mut state_mut, state: String); + _impl!(get_mut account_id_mut, account_id: Id); + _impl!(get_mut state_mut, state: State); _impl!(get_mut list_mut, list: Vec); - _impl!(get_mut not_found_mut, not_found: Vec); + _impl!(get_mut not_found_mut, not_found: Vec>); } #[derive(Deserialize, Debug)] @@ -264,7 +439,7 @@ pub struct Query, OBJ: Object> where OBJ: std::fmt::Debug + Serialize, { - account_id: String, + account_id: Id, filter: Option, sort: Option>, #[serde(default)] @@ -288,7 +463,7 @@ where { pub fn new() -> Self { Self { - account_id: String::new(), + account_id: Id::new(), filter: None, sort: None, position: 0, @@ -300,7 +475,7 @@ where } } - _impl!(account_id: String); + _impl!(account_id: Id); _impl!(filter: Option); _impl!(sort: Option>); _impl!(position: u64); @@ -325,12 +500,11 @@ pub fn bool_true() -> bool { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryResponse { - #[serde(skip_serializing_if = "String::is_empty", default)] - pub account_id: String, + pub account_id: Id, pub query_state: String, pub can_calculate_changes: bool, pub position: u64, - pub ids: Vec, + pub ids: Vec>, #[serde(default)] pub total: u64, #[serde(default)] @@ -349,7 +523,7 @@ impl std::convert::TryFrom<&RawValue> for QueryR } impl QueryResponse { - _impl!(get_mut ids_mut, ids: Vec); + _impl!(get_mut ids_mut, ids: Vec>); } pub struct ResultField, OBJ: Object> { @@ -410,9 +584,8 @@ pub struct Changes where OBJ: std::fmt::Debug + Serialize, { - #[serde(skip_serializing_if = "String::is_empty")] - pub account_id: String, - pub since_state: String, + pub account_id: Id, + pub since_state: State, #[serde(skip_serializing_if = "Option::is_none")] pub max_changes: Option, #[serde(skip)] @@ -425,8 +598,8 @@ where { pub fn new() -> Self { Self { - account_id: String::new(), - since_state: String::new(), + account_id: Id::new(), + since_state: State::new(), max_changes: None, _ph: PhantomData, } @@ -436,7 +609,7 @@ where /// /// The id of the account to use. /// - account_id: String + account_id: Id ); _impl!( /// - since_state: "String" @@ -446,7 +619,7 @@ where /// state. /// /// - since_state: String + since_state: State ); _impl!( /// - max_changes: "UnsignedInt|null" @@ -463,14 +636,13 @@ where #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ChangesResponse { - #[serde(skip_serializing_if = "String::is_empty")] - pub account_id: String, - pub old_state: String, - pub new_state: String, + pub account_id: Id, + pub old_state: State, + pub new_state: State, pub has_more_changes: bool, - pub created: Vec, - pub updated: Vec, - pub destroyed: Vec, + pub created: Vec>, + pub updated: Vec>, + pub destroyed: Vec>, #[serde(skip)] pub _ph: PhantomData OBJ>, } @@ -485,13 +657,13 @@ impl std::convert::TryFrom<&RawValue> for Change } impl ChangesResponse { - _impl!(get_mut account_id_mut, account_id: String); - _impl!(get_mut old_state_mut, old_state: String); - _impl!(get_mut new_state_mut, new_state: String); + _impl!(get_mut account_id_mut, account_id: Id); + _impl!(get_mut old_state_mut, old_state: State); + _impl!(get_mut new_state_mut, new_state: State); _impl!(get has_more_changes, has_more_changes: bool); - _impl!(get_mut created_mut, created: Vec); - _impl!(get_mut updated_mut, updated: Vec); - _impl!(get_mut destroyed_mut, destroyed: Vec); + _impl!(get_mut created_mut, created: Vec>); + _impl!(get_mut updated_mut, updated: Vec>); + _impl!(get_mut destroyed_mut, destroyed: Vec>); } ///#`set` @@ -508,11 +680,10 @@ pub struct Set where OBJ: std::fmt::Debug + Serialize, { - #[serde(skip_serializing_if = "String::is_empty")] ///o accountId: "Id" /// /// The id of the account to use. - pub account_id: String, + pub account_id: Id, ///o ifInState: "String|null" /// /// This is a state string as returned by the "Foo/get" method @@ -521,7 +692,7 @@ where /// otherwise, the method will be aborted and a "stateMismatch" error /// returned. If null, any changes will be applied to the current /// state. - pub if_in_state: Option, + pub if_in_state: Option>, ///o create: "Id[Foo]|null" /// /// A map of a *creation id* (a temporary id set by the client) to Foo @@ -533,7 +704,7 @@ where /// The client MUST omit any properties that may only be set by the /// server (for example, the "id" property on most object types). /// - pub create: Option>, + pub create: Option, OBJ>>, ///o update: "Id[PatchObject]|null" /// /// A map of an id to a Patch object to apply to the current Foo @@ -577,12 +748,12 @@ where /// is also a valid PatchObject. The client may choose to optimise /// network usage by just sending the diff or may send the whole /// object; the server processes it the same either way. - pub update: Option>, + pub update: Option, Value>>, ///o destroy: "Id[]|null" /// /// A list of ids for Foo objects to permanently delete, or null if no /// objects are to be destroyed. - pub destroy: Option>, + pub destroy: Option>>, } impl Set @@ -591,14 +762,14 @@ where { pub fn new() -> Self { Self { - account_id: String::new(), + account_id: Id::new(), if_in_state: None, create: None, update: None, destroy: None, } } - _impl!(account_id: String); + _impl!(account_id: Id); _impl!( ///o ifInState: "String|null" /// @@ -608,9 +779,9 @@ where /// otherwise, the method will be aborted and a "stateMismatch" error /// returned. If null, any changes will be applied to the current /// state. - if_in_state: Option + if_in_state: Option> ); - _impl!(update: Option>); + _impl!(update: Option, Value>>); } #[derive(Serialize, Deserialize, Debug)] @@ -619,17 +790,17 @@ pub struct SetResponse { ///o accountId: "Id" /// /// The id of the account used for the call. - pub account_id: String, + pub account_id: Id, ///o oldState: "String|null" /// /// The state string that would have been returned by "Foo/get" before /// making the requested changes, or null if the server doesn't know /// what the previous state string was. - pub old_state: String, + pub old_state: State, ///o newState: "String" /// /// The state string that will now be returned by "Foo/get". - pub new_state: String, + pub new_state: State, ///o created: "Id[Foo]|null" /// /// A map of the creation id to an object containing any properties of @@ -639,7 +810,7 @@ pub struct SetResponse { /// and thus set to a default by the server. /// /// This argument is null if no Foo objects were successfully created. - pub created: Option>, + pub created: Option, OBJ>>, ///o updated: "Id[Foo|null]|null" /// /// The keys in this map are the ids of all Foos that were @@ -651,12 +822,12 @@ pub struct SetResponse { /// any changes to server-set or computed properties. /// /// This argument is null if no Foo objects were successfully updated. - pub updated: Option>>, + pub updated: Option, Option>>, ///o destroyed: "Id[]|null" /// /// A list of Foo ids for records that were successfully destroyed, or /// null if none. - pub destroyed: Option>, + pub destroyed: Option>>, ///o notCreated: "Id[SetError]|null" /// /// A map of the creation id to a SetError object for each record that @@ -760,8 +931,8 @@ impl core::fmt::Display for SetError { pub fn download_request_format( session: &JmapSession, - account_id: &Id, - blob_id: &Id, + account_id: &Id, + blob_id: &Id, name: Option, ) -> String { // https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} @@ -777,10 +948,10 @@ pub fn download_request_format( ret.push_str(&session.download_url[prev_pos..prev_pos + pos]); prev_pos += pos; if session.download_url[prev_pos..].starts_with("{accountId}") { - ret.push_str(account_id); + ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); } else if session.download_url[prev_pos..].starts_with("{blobId}") { - ret.push_str(blob_id); + ret.push_str(blob_id.as_str()); prev_pos += "{blobId}".len(); } else if session.download_url[prev_pos..].starts_with("{name}") { ret.push_str(name.as_ref().map(String::as_str).unwrap_or("")); @@ -793,7 +964,7 @@ pub fn download_request_format( ret } -pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { +pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { //"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", let mut ret = String::with_capacity(session.upload_url.len() + account_id.len()); let mut prev_pos = 0; @@ -802,7 +973,7 @@ pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { ret.push_str(&session.upload_url[prev_pos..prev_pos + pos]); prev_pos += pos; if session.upload_url[prev_pos..].starts_with("{accountId}") { - ret.push_str(account_id); + ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); break; } else { @@ -822,12 +993,12 @@ pub struct UploadResponse { ///o accountId: "Id" /// /// The id of the account used for the call. - pub account_id: String, + pub account_id: Id, ///o blobId: "Id" /// ///The id representing the binary data uploaded. The data for this id is immutable. ///The id *only* refers to the binary data, not any metadata. - pub blob_id: String, + pub blob_id: Id, ///o type: "String" /// ///The media type of the file (as specified in [RFC6838],