melib/jmap: add Type parameter to Id, State

Make Id, State have a type parameter to the object it refers to (eg
`Id<EmailObject>`) instead of just a String
jmap-eventsource
Manos Pitsidianakis 2020-09-21 19:13:44 +03:00
parent 19d4a191d8
commit 425f4b9930
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
8 changed files with 375 additions and 203 deletions

View File

@ -191,15 +191,16 @@ macro_rules! get_conf_val {
pub struct Store { pub struct Store {
pub account_name: Arc<String>, pub account_name: Arc<String>,
pub account_hash: AccountHash, pub account_hash: AccountHash,
pub account_id: Arc<Mutex<String>>, pub account_id: Arc<Mutex<Id<Account>>>,
pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>, pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id>>>, pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id, EnvelopeHash>>>, pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id>>>, pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>, pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>, pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>, pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub object_set_states: Arc<Mutex<HashMap<&'static str, String>>>, pub email_state: Arc<Mutex<State<EmailObject>>>,
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>, pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
pub is_subscribed: Arc<IsSubscribedFn>, pub is_subscribed: Arc<IsSubscribedFn>,
pub event_consumer: BackendEventConsumer, pub event_consumer: BackendEventConsumer,
@ -275,7 +276,7 @@ impl Store {
pub fn remove_envelope( pub fn remove_envelope(
&self, &self,
obj_id: Id, obj_id: Id<EmailObject>,
) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> { ) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> {
let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?; let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?;
self.id_store.lock().unwrap().remove(&env_hash); self.id_store.lock().unwrap().remove(&env_hash);
@ -413,7 +414,7 @@ impl MailBackend for JmapType {
) )
.await?; .await?;
let mailbox_id: String = { let mailbox_id: Id<MailboxObject> = {
let mailboxes_lck = store.mailboxes.read().unwrap(); let mailboxes_lck = store.mailboxes.read().unwrap();
if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) { if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) {
mailbox.id.clone() mailbox.id.clone()
@ -428,7 +429,7 @@ impl MailBackend for JmapType {
let upload_response: UploadResponse = serde_json::from_str(&res_text)?; let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
let mut req = Request::new(conn.request_no.clone()); let mut req = Request::new(conn.request_no.clone());
let creation_id = "1".to_string(); let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default(); let mut email_imports = HashMap::default();
let mut mailbox_ids = HashMap::default(); let mut mailbox_ids = HashMap::default();
mailbox_ids.insert(mailbox_id, true); mailbox_ids.insert(mailbox_id, true);
@ -440,7 +441,7 @@ impl MailBackend for JmapType {
); );
let import_call: ImportCall = ImportCall::new() let import_call: ImportCall = ImportCall::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.emails(email_imports); .emails(email_imports);
req.add_call(&import_call); req.add_call(&import_call);
@ -508,7 +509,7 @@ impl MailBackend for JmapType {
conn.connect().await?; conn.connect().await?;
let email_call: EmailQuery = EmailQuery::new( let email_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.filter(Some(filter)) .filter(Some(filter))
.position(0), .position(0),
) )
@ -527,14 +528,7 @@ impl MailBackend for JmapType {
*store.online_status.lock().await = (std::time::Instant::now(), Ok(())); *store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?; let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m; let QueryResponse::<EmailObject> { ids, .. } = m;
let ret = ids let ret = ids.into_iter().map(|id| id.into_hash()).collect();
.into_iter()
.map(|id| {
let mut h = DefaultHasher::new();
h.write(id.as_bytes());
h.finish()
})
.collect();
Ok(ret) Ok(ret)
})) }))
} }
@ -584,9 +578,9 @@ impl MailBackend for JmapType {
mailboxes_lck[&destination_mailbox_hash].id.clone(), mailboxes_lck[&destination_mailbox_hash].id.clone(),
) )
}; };
let mut update_map: HashMap<String, Value> = HashMap::default(); let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1); let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default(); let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default(); let mut update_keywords: HashMap<String, Value> = HashMap::default();
update_keywords.insert( update_keywords.insert(
format!("mailboxIds/{}", &destination_mailbox_id), format!("mailboxIds/{}", &destination_mailbox_id),
@ -594,7 +588,7 @@ impl MailBackend for JmapType {
); );
if move_ { if move_ {
update_keywords.insert( update_keywords.insert(
format!("mailboxIds/{}", &source_mailbox_hash), format!("mailboxIds/{}", &source_mailbox_id),
serde_json::json!(null), serde_json::json!(null),
); );
} }
@ -611,7 +605,7 @@ impl MailBackend for JmapType {
let email_set_call: EmailSet = EmailSet::new( let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new() Set::<EmailObject>::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.update(Some(update_map)), .update(Some(update_map)),
); );
@ -653,9 +647,9 @@ impl MailBackend for JmapType {
let connection = self.connection.clone(); let connection = self.connection.clone();
Ok(Box::pin(async move { Ok(Box::pin(async move {
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone(); let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let mut update_map: HashMap<String, Value> = HashMap::default(); let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1); let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default(); let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default(); let mut update_keywords: HashMap<String, Value> = HashMap::default();
for (flag, value) in flags.iter() { for (flag, value) in flags.iter() {
match flag { 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( let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new() Set::<EmailObject>::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.update(Some(update_map)), .update(Some(update_map)),
); );
@ -714,7 +708,7 @@ impl MailBackend for JmapType {
let email_call: EmailGet = EmailGet::new( let email_call: EmailGet = EmailGet::new(
Get::new() Get::new()
.ids(Some(JmapArgument::Value(ids))) .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()])), .properties(Some(vec!["keywords".to_string()])),
); );
@ -759,21 +753,15 @@ impl MailBackend for JmapType {
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?; let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, state, .. } = e; let GetResponse::<EmailObject> { list, state, .. } = e;
{ {
let c = store let (is_empty, is_equal) = {
.object_set_states let current_state_lck = conn.store.email_state.lock().unwrap();
.lock() (current_state_lck.is_empty(), *current_state_lck != state)
.unwrap() };
.get(&EmailObject::NAME) if is_empty {
.map(|prev_state| *prev_state == state);
if let Some(false) = c {
conn.email_changes().await?;
} else {
debug!("{:?}: inserting state {}", EmailObject::NAME, &state); debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
store *conn.store.email_state.lock().unwrap() = state;
.object_set_states } else if !is_equal {
.lock() conn.email_changes().await?;
.unwrap()
.insert(EmailObject::NAME, state);
} }
} }
debug!(&list); debug!(&list);
@ -813,7 +801,7 @@ impl JmapType {
let store = Arc::new(Store { let store = Arc::new(Store {
account_name: Arc::new(s.name.clone()), account_name: Arc::new(s.name.clone()),
account_hash, account_hash,
account_id: Arc::new(Mutex::new(String::new())), account_id: Arc::new(Mutex::new(Id::new())),
online_status, online_status,
event_consumer, event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
@ -825,7 +813,8 @@ impl JmapType {
tag_index: Default::default(), tag_index: Default::default(),
mailboxes: Default::default(), mailboxes: Default::default(),
mailboxes_index: Default::default(), mailboxes_index: Default::default(),
object_set_states: Default::default(), email_state: Default::default(),
mailbox_state: Default::default(),
}); });
Ok(Box::new(JmapType { Ok(Box::new(JmapType {

View File

@ -100,7 +100,7 @@ impl JmapConnection {
Ok(()) Ok(())
} }
pub fn mail_account_id(&self) -> &Id { pub fn mail_account_id(&self) -> &Id<Account> {
&self.session.primary_accounts["urn:ietf:params:jmap:mail"] &self.session.primary_accounts["urn:ietf:params:jmap:mail"]
} }
@ -109,22 +109,16 @@ impl JmapConnection {
} }
pub async fn email_changes(&self) -> Result<()> { pub async fn email_changes(&self) -> Result<()> {
let mut current_state: String = { let mut current_state: State<EmailObject> = self.store.email_state.lock().unwrap().clone();
let object_set_states_lck = self.store.object_set_states.lock().unwrap(); if current_state.is_empty() {
let v = if let Some(prev_state) = debug!(object_set_states_lck.get(&EmailObject::NAME)) debug!("{:?}: has no saved state", EmailObject::NAME);
{ return Ok(());
prev_state.clone() }
} else {
return Ok(());
};
drop(object_set_states_lck);
v
};
loop { loop {
let email_changes_call: EmailChanges = EmailChanges::new( let email_changes_call: EmailChanges = EmailChanges::new(
Changes::<EmailObject>::new() Changes::<EmailObject>::new()
.account_id(self.mail_account_id().to_string()) .account_id(self.mail_account_id().clone())
.since_state(current_state.clone()), .since_state(current_state.clone()),
); );
@ -136,7 +130,7 @@ impl JmapConnection {
prev_seq, prev_seq,
ResultField::<EmailChanges, EmailObject>::new("created"), ResultField::<EmailChanges, EmailObject>::new("created"),
))) )))
.account_id(self.mail_account_id().to_string()), .account_id(self.mail_account_id().clone()),
); );
req.add_call(&email_get_call); req.add_call(&email_get_call);
@ -153,6 +147,12 @@ impl JmapConnection {
GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?; GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
debug!(&get_response); debug!(&get_response);
let GetResponse::<EmailObject> { list, .. } = get_response; let GetResponse::<EmailObject> { list, .. } = get_response;
let changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
if changes_response.new_state == current_state {
return Ok(());
}
let mut mailbox_hashes: Vec<SmallVec<[MailboxHash; 8]>> = let mut mailbox_hashes: Vec<SmallVec<[MailboxHash; 8]>> =
Vec::with_capacity(list.len()); Vec::with_capacity(list.len());
for envobj in &list { for envobj in &list {
@ -189,9 +189,6 @@ impl JmapConnection {
} }
} }
let changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let ChangesResponse::<EmailObject> { let ChangesResponse::<EmailObject> {
account_id: _, account_id: _,
new_state, new_state,
@ -218,11 +215,7 @@ impl JmapConnection {
if has_more_changes { if has_more_changes {
current_state = new_state; current_state = new_state;
} else { } else {
self.store *self.store.email_state.lock().unwrap() = new_state;
.object_set_states
.lock()
.unwrap()
.insert(EmailObject::NAME, new_state);
break; break;
} }
} }

View File

@ -29,10 +29,11 @@ pub struct JmapMailbox {
pub path: String, pub path: String,
pub hash: MailboxHash, pub hash: MailboxHash,
pub children: Vec<MailboxHash>, pub children: Vec<MailboxHash>,
pub id: String, pub id: Id<MailboxObject>,
pub is_subscribed: bool, pub is_subscribed: bool,
pub my_rights: JmapRights, pub my_rights: JmapRights,
pub parent_id: Option<String>, pub parent_id: Option<Id<MailboxObject>>,
pub parent_hash: Option<MailboxHash>,
pub role: Option<String>, pub role: Option<String>,
pub sort_order: u64, pub sort_order: u64,
pub total_emails: Arc<Mutex<u64>>, pub total_emails: Arc<Mutex<u64>>,
@ -66,7 +67,7 @@ impl BackendMailbox for JmapMailbox {
} }
fn parent(&self) -> Option<MailboxHash> { fn parent(&self) -> Option<MailboxHash> {
None self.parent_hash
} }
fn permissions(&self) -> MailboxPermissions { fn permissions(&self) -> MailboxPermissions {

View File

@ -32,6 +32,21 @@ use std::hash::Hasher;
mod import; mod import;
pub use import::*; pub use import::*;
#[derive(Debug)]
pub struct ThreadObject;
impl Object for ThreadObject {
const NAME: &'static str = "Thread";
}
impl Id<EmailObject> {
pub fn into_hash(&self) -> EnvelopeHash {
let mut h = DefaultHasher::new();
h.write(self.inner.as_bytes());
h.finish()
}
}
// 4.1.1. // 4.1.1.
// Metadata // Metadata
// These properties represent metadata about the message in the mail // These properties represent metadata about the message in the mail
@ -133,11 +148,11 @@ pub use import::*;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EmailObject { pub struct EmailObject {
#[serde(default)] #[serde(default)]
pub id: Id, pub id: Id<EmailObject>,
#[serde(default)] #[serde(default)]
pub blob_id: String, pub blob_id: Id<BlobObject>,
#[serde(default)] #[serde(default)]
pub mailbox_ids: HashMap<Id, bool>, pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
#[serde(default)] #[serde(default)]
pub size: u64, pub size: u64,
#[serde(default)] #[serde(default)]
@ -163,7 +178,7 @@ pub struct EmailObject {
#[serde(default)] #[serde(default)]
pub keywords: HashMap<String, bool>, pub keywords: HashMap<String, bool>,
#[serde(default)] #[serde(default)]
pub attached_emails: Option<Id>, pub attached_emails: Option<Id<BlobObject>>,
#[serde(default)] #[serde(default)]
pub attachments: Vec<Value>, pub attachments: Vec<Value>,
#[serde(default)] #[serde(default)]
@ -182,7 +197,7 @@ pub struct EmailObject {
#[serde(default)] #[serde(default)]
pub text_body: Vec<TextBody>, pub text_body: Vec<TextBody>,
#[serde(default)] #[serde(default)]
pub thread_id: Id, pub thread_id: Id<ThreadObject>,
#[serde(flatten)] #[serde(flatten)]
pub extra: HashMap<String, Value>, pub extra: HashMap<String, Value>,
} }
@ -313,9 +328,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
} }
} }
let mut h = DefaultHasher::new(); env.set_hash(t.id.into_hash());
h.write(t.id.as_bytes());
env.set_hash(h.finish());
env env
} }
} }
@ -323,7 +336,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HtmlBody { pub struct HtmlBody {
pub blob_id: Id, pub blob_id: Id<BlobObject>,
#[serde(default)] #[serde(default)]
pub charset: String, pub charset: String,
#[serde(default)] #[serde(default)]
@ -350,7 +363,7 @@ pub struct HtmlBody {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TextBody { pub struct TextBody {
pub blob_id: Id, pub blob_id: Id<BlobObject>,
#[serde(default)] #[serde(default)]
pub charset: String, pub charset: String,
#[serde(default)] #[serde(default)]
@ -381,12 +394,12 @@ impl Object for EmailObject {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EmailQueryResponse { pub struct EmailQueryResponse {
pub account_id: Id, pub account_id: Id<Account>,
pub can_calculate_changes: bool, pub can_calculate_changes: bool,
pub collapse_threads: bool, pub collapse_threads: bool,
// FIXME // FIXME
pub filter: String, pub filter: String,
pub ids: Vec<Id>, pub ids: Vec<Id<EmailObject>>,
pub position: u64, pub position: u64,
pub query_state: String, pub query_state: String,
pub sort: Option<String>, pub sort: Option<String>,
@ -469,9 +482,9 @@ impl EmailGet {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EmailFilterCondition { pub struct EmailFilterCondition {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub in_mailbox: Option<Id>, pub in_mailbox: Option<Id<MailboxObject>>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub in_mailbox_other_than: Vec<Id>, pub in_mailbox_other_than: Vec<Id<MailboxObject>>,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
pub before: UtcDate, pub before: UtcDate,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
@ -517,8 +530,8 @@ impl EmailFilterCondition {
Self::default() Self::default()
} }
_impl!(in_mailbox: Option<Id>); _impl!(in_mailbox: Option<Id<MailboxObject>>);
_impl!(in_mailbox_other_than: Vec<Id>); _impl!(in_mailbox_other_than: Vec<Id<MailboxObject>>);
_impl!(before: UtcDate); _impl!(before: UtcDate);
_impl!(after: UtcDate); _impl!(after: UtcDate);
_impl!(min_size: Option<u64>); _impl!(min_size: Option<u64>);
@ -728,7 +741,7 @@ fn test_jmap_query() {
let mut r = Filter::Condition( let mut r = Filter::Condition(
EmailFilterCondition::new() EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id)) .in_mailbox(Some(mailbox_id.into()))
.into(), .into(),
); );
r &= f; r &= f;
@ -737,7 +750,7 @@ fn test_jmap_query() {
let email_call: EmailQuery = EmailQuery::new( let email_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id("account_id".to_string()) .account_id("account_id".to_string().into())
.filter(Some(filter)) .filter(Some(filter))
.position(0), .position(0),
) )

View File

@ -37,7 +37,7 @@ use serde_json::value::RawValue;
pub struct ImportCall { pub struct ImportCall {
///accountId: "Id" ///accountId: "Id"
///The id of the account to use. ///The id of the account to use.
pub account_id: String, pub account_id: Id<Account>,
///ifInState: "String|null" ///ifInState: "String|null"
///This is a state string as returned by the "Email/get" method. If ///This is a state string as returned by the "Email/get" method. If
///supplied, the string must match the current state of the account ///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 ///and a "stateMismatch" error returned. If null, any changes will
///be applied to the current state. ///be applied to the current state.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub if_in_state: Option<String>, pub if_in_state: Option<State<EmailObject>>,
///o emails: "Id[EmailImport]" ///o emails: "Id[EmailImport]"
///A map of creation id (client specified) to EmailImport objects. ///A map of creation id (client specified) to EmailImport objects.
pub emails: HashMap<Id, EmailImport>, pub emails: HashMap<Id<EmailObject>, EmailImport>,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -56,11 +56,11 @@ pub struct ImportCall {
pub struct EmailImport { pub struct EmailImport {
///o blobId: "Id" ///o blobId: "Id"
///The id of the blob containing the raw message [RFC5322]. ///The id of the blob containing the raw message [RFC5322].
pub blob_id: String, pub blob_id: Id<BlobObject>,
///o mailboxIds: "Id[Boolean]" ///o mailboxIds: "Id[Boolean]"
///The ids of the Mailboxes to assign this Email to. At least one ///The ids of the Mailboxes to assign this Email to. At least one
///Mailbox MUST be given. ///Mailbox MUST be given.
pub mailbox_ids: HashMap<Id, bool>, pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
///o keywords: "String[Boolean]" (default: {}) ///o keywords: "String[Boolean]" (default: {})
///The keywords to apply to the Email. ///The keywords to apply to the Email.
pub keywords: HashMap<String, bool>, pub keywords: HashMap<String, bool>,
@ -74,7 +74,7 @@ pub struct EmailImport {
impl ImportCall { impl ImportCall {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
account_id: String::new(), account_id: Id::new(),
if_in_state: None, if_in_state: None,
emails: HashMap::default(), emails: HashMap::default(),
} }
@ -85,10 +85,10 @@ impl ImportCall {
/// ///
/// The id of the account to use. /// The id of the account to use.
/// ///
account_id: String account_id: Id<Account>
); );
_impl!(if_in_state: Option<String>); _impl!(if_in_state: Option<State<EmailObject>>);
_impl!(emails: HashMap<Id, EmailImport>); _impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
} }
impl Method<EmailObject> for ImportCall { impl Method<EmailObject> for ImportCall {
@ -98,15 +98,15 @@ impl Method<EmailObject> for ImportCall {
impl EmailImport { impl EmailImport {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
blob_id: String::new(), blob_id: Id::new(),
mailbox_ids: HashMap::default(), mailbox_ids: HashMap::default(),
keywords: HashMap::default(), keywords: HashMap::default(),
received_at: None, received_at: None,
} }
} }
_impl!(blob_id: String); _impl!(blob_id: Id<BlobObject>);
_impl!(mailbox_ids: HashMap<Id, bool>); _impl!(mailbox_ids: HashMap<Id<MailboxObject>, bool>);
_impl!(keywords: HashMap<String, bool>); _impl!(keywords: HashMap<String, bool>);
_impl!(received_at: Option<String>); _impl!(received_at: Option<String>);
} }
@ -126,7 +126,7 @@ pub enum ImportError {
///the SetError object with the id of the existing Email. If duplicates ///the SetError object with the id of the existing Email. If duplicates
///are allowed, the newly created Email object MUST have a separate id ///are allowed, the newly created Email object MUST have a separate id
///and independent mutable properties to the existing object. ///and independent mutable properties to the existing object.
existing_id: Id, existing_id: Id<EmailObject>,
}, },
///If the "blobId", "mailboxIds", or "keywords" properties are invalid ///If the "blobId", "mailboxIds", or "keywords" properties are invalid
///(e.g., missing, wrong type, id not found), the server MUST reject the ///(e.g., missing, wrong type, id not found), the server MUST reject the
@ -155,30 +155,30 @@ pub enum ImportError {
pub struct ImportResponse { pub struct ImportResponse {
///o accountId: "Id" ///o accountId: "Id"
///The id of the account used for this call. ///The id of the account used for this call.
pub account_id: Id, pub account_id: Id<Account>,
///o oldState: "String|null" ///o oldState: "String|null"
///The state string that would have been returned by "Email/get" on ///The state string that would have been returned by "Email/get" on
///this account before making the requested changes, or null if the ///this account before making the requested changes, or null if the
///server doesn't know what the previous state string was. ///server doesn't know what the previous state string was.
pub old_state: Option<String>, pub old_state: Option<State<EmailObject>>,
///o newState: "String" ///o newState: "String"
///The state string that will now be returned by "Email/get" on this ///The state string that will now be returned by "Email/get" on this
///account. ///account.
pub new_state: Option<String>, pub new_state: Option<State<EmailObject>>,
///o created: "Id[Email]|null" ///o created: "Id[Email]|null"
///A map of the creation id to an object containing the "id", ///A map of the creation id to an object containing the "id",
///"blobId", "threadId", and "size" properties for each successfully ///"blobId", "threadId", and "size" properties for each successfully
///imported Email, or null if none. ///imported Email, or null if none.
pub created: HashMap<Id, ImportEmailResult>, pub created: HashMap<Id<EmailObject>, ImportEmailResult>,
///o notCreated: "Id[SetError]|null" ///o notCreated: "Id[SetError]|null"
///A map of the creation id to a SetError object for each Email that ///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 ///failed to be created, or null if all successful. The possible
///errors are defined above. ///errors are defined above.
pub not_created: HashMap<Id, ImportError>, pub not_created: HashMap<Id<EmailObject>, ImportError>,
} }
impl std::convert::TryFrom<&RawValue> for ImportResponse { impl std::convert::TryFrom<&RawValue> for ImportResponse {
@ -193,8 +193,8 @@ impl std::convert::TryFrom<&RawValue> for ImportResponse {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ImportEmailResult { pub struct ImportEmailResult {
pub id: Id, pub id: Id<EmailObject>,
pub blob_id: Id, pub blob_id: Id<BlobObject>,
pub thread_id: Id, pub thread_id: Id<ThreadObject>,
pub size: usize, pub size: usize,
} }

View File

@ -21,14 +21,22 @@
use super::*; use super::*;
impl Id<MailboxObject> {
pub fn into_hash(&self) -> MailboxHash {
let mut h = DefaultHasher::new();
h.write(self.inner.as_bytes());
h.finish()
}
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MailboxObject { pub struct MailboxObject {
pub id: String, pub id: Id<MailboxObject>,
pub is_subscribed: bool, pub is_subscribed: bool,
pub my_rights: JmapRights, pub my_rights: JmapRights,
pub name: String, pub name: String,
pub parent_id: Option<String>, pub parent_id: Option<Id<MailboxObject>>,
pub role: Option<String>, pub role: Option<String>,
pub sort_order: u64, pub sort_order: u64,
pub total_emails: u64, pub total_emails: u64,

View File

@ -25,7 +25,6 @@ use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::convert::TryFrom; use std::convert::TryFrom;
pub type Id = String;
pub type UtcDate = String; pub type UtcDate = String;
use super::rfc8620::Object; use super::rfc8620::Object;
@ -125,7 +124,8 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
unread_emails, unread_emails,
unread_threads, unread_threads,
} = r; } = r;
let hash = crate::get_path_hash!(&name); let hash = id.into_hash();
let parent_hash = parent_id.clone().map(|id| id.into_hash());
( (
hash, hash,
JmapMailbox { JmapMailbox {
@ -137,6 +137,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
is_subscribed, is_subscribed,
my_rights, my_rights,
parent_id, parent_id,
parent_hash,
role, role,
usage: Default::default(), usage: Default::default(),
sort_order, sort_order,
@ -150,10 +151,13 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
.collect()) .collect())
} }
pub async fn get_message_list(conn: &JmapConnection, mailbox: &JmapMailbox) -> Result<Vec<String>> { pub async fn get_message_list(
conn: &JmapConnection,
mailbox: &JmapMailbox,
) -> Result<Vec<Id<EmailObject>>> {
let email_call: EmailQuery = EmailQuery::new( let email_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition( .filter(Some(Filter::Condition(
EmailFilterCondition::new() EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone())) .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 mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new( let email_query_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id().to_string()) .account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition( .filter(Some(Filter::Condition(
EmailFilterCondition::new() EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id)) .in_mailbox(Some(mailbox_id))
@ -232,7 +236,7 @@ pub async fn fetch(
prev_seq, prev_seq,
EmailQuery::RESULT_FIELD_IDS, EmailQuery::RESULT_FIELD_IDS,
))) )))
.account_id(conn.mail_account_id().to_string()), .account_id(conn.mail_account_id().clone()),
); );
req.add_call(&email_call); req.add_call(&email_call);
@ -248,22 +252,15 @@ pub async fn fetch(
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?; let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, state, .. } = e; let GetResponse::<EmailObject> { list, state, .. } = e;
{ {
let v = conn let (is_empty, is_equal) = {
.store let current_state_lck = conn.store.email_state.lock().unwrap();
.object_set_states (current_state_lck.is_empty(), *current_state_lck != state)
.lock() };
.unwrap() if is_empty {
.get(&EmailObject::NAME)
.map(|prev_state| *prev_state == state);
if let Some(false) = v {
conn.email_changes().await?;
} else {
debug!("{:?}: inserting state {}", EmailObject::NAME, &state); debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
conn.store *conn.store.email_state.lock().unwrap() = state;
.object_set_states } else if !is_equal {
.lock() conn.email_changes().await?;
.unwrap()
.insert(EmailObject::NAME, state);
} }
} }
let mut ret = Vec::with_capacity(list.len()); let mut ret = Vec::with_capacity(list.len());

View File

@ -19,12 +19,12 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
use super::Id;
use crate::email::parser::BytesExt; use crate::email::parser::BytesExt;
use core::marker::PhantomData; use core::marker::PhantomData;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde::ser::{Serialize, SerializeStruct, Serializer};
use serde_json::{value::RawValue, Value}; use serde_json::{value::RawValue, Value};
use std::hash::{Hash, Hasher};
mod filters; mod filters;
pub use filters::*; pub use filters::*;
@ -39,23 +39,189 @@ pub trait Object {
const NAME: &'static str; const NAME: &'static str;
} }
#[derive(Deserialize, Serialize)]
#[serde(transparent)]
pub struct Id<OBJ> {
pub inner: String,
#[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>,
}
impl<OBJ: Object> core::fmt::Debug for Id<OBJ> {
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<String> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("Id<Any>").field(&self.inner).finish()
}
}
//, Hash, Eq, PartialEq, Default)]
impl<OBJ> Clone for Id<OBJ> {
fn clone(&self) -> Self {
Id {
inner: self.inner.clone(),
_ph: PhantomData,
}
}
}
impl<OBJ> std::cmp::Eq for Id<OBJ> {}
impl<OBJ> std::cmp::PartialEq for Id<OBJ> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<OBJ> Hash for Id<OBJ> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<OBJ> Default for Id<OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<OBJ> From<String> for Id<OBJ> {
fn from(inner: String) -> Self {
Id {
inner,
_ph: PhantomData,
}
}
}
impl<OBJ> core::fmt::Display for Id<OBJ> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.inner, fmt)
}
}
impl<OBJ> Id<OBJ> {
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<OBJ> {
pub inner: String,
#[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>,
}
//, Hash, Eq, PartialEq, Default)]
impl<OBJ> Clone for State<OBJ> {
fn clone(&self) -> Self {
State {
inner: self.inner.clone(),
_ph: PhantomData,
}
}
}
impl<OBJ> std::cmp::Eq for State<OBJ> {}
impl<OBJ> std::cmp::PartialEq for State<OBJ> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<OBJ> Hash for State<OBJ> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<OBJ> Default for State<OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<OBJ> From<String> for State<OBJ> {
fn from(inner: String) -> Self {
State {
inner,
_ph: PhantomData,
}
}
}
impl<OBJ> core::fmt::Display for State<OBJ> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.inner, fmt)
}
}
impl<OBJ> State<OBJ> {
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)] #[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct JmapSession { pub struct JmapSession {
pub capabilities: HashMap<String, CapabilitiesObject>, pub capabilities: HashMap<String, CapabilitiesObject>,
pub accounts: HashMap<Id, Account>, pub accounts: HashMap<Id<Account>, Account>,
pub primary_accounts: HashMap<String, Id>, pub primary_accounts: HashMap<String, Id<Account>>,
pub username: String, pub username: String,
pub api_url: String, pub api_url: String,
pub download_url: String, pub download_url: String,
pub upload_url: String, pub upload_url: String,
pub event_source_url: String, pub event_source_url: String,
pub state: String, pub state: State<JmapSession>,
#[serde(flatten)] #[serde(flatten)]
pub extra_properties: HashMap<String, Value>, pub extra_properties: HashMap<String, Value>,
} }
impl Object for JmapSession {
const NAME: &'static str = "Session";
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject { pub struct CapabilitiesObject {
@ -88,6 +254,17 @@ pub struct Account {
extra_properties: HashMap<String, Value>, extra_properties: HashMap<String, Value>,
} }
impl Object for Account {
const NAME: &'static str = "Account";
}
#[derive(Debug)]
pub struct BlobObject;
impl Object for BlobObject {
const NAME: &'static str = "Blob";
}
/// #`get` /// #`get`
/// ///
/// Objects of type `Foo` are fetched via a call to `Foo/get`. /// Objects of type `Foo` are fetched via a call to `Foo/get`.
@ -104,11 +281,10 @@ pub struct Get<OBJ: Object>
where where
OBJ: std::fmt::Debug + Serialize, OBJ: std::fmt::Debug + Serialize,
{ {
#[serde(skip_serializing_if = "String::is_empty")] pub account_id: Id<Account>,
pub account_id: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)] #[serde(flatten)]
pub ids: Option<JmapArgument<Vec<String>>>, pub ids: Option<JmapArgument<Vec<Id<OBJ>>>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<String>>, pub properties: Option<Vec<String>>,
#[serde(skip)] #[serde(skip)]
@ -121,7 +297,7 @@ where
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
account_id: String::new(), account_id: Id::new(),
ids: None, ids: None,
properties: None, properties: None,
_ph: PhantomData, _ph: PhantomData,
@ -132,7 +308,7 @@ where
/// ///
/// The id of the account to use. /// The id of the account to use.
/// ///
account_id: String account_id: Id<Account>
); );
_impl!( _impl!(
/// - ids: `Option<JmapArgument<Vec<String>>>` /// - ids: `Option<JmapArgument<Vec<String>>>`
@ -142,7 +318,7 @@ where
/// type and the number of records does not exceed the /// type and the number of records does not exceed the
/// "max_objects_in_get" limit. /// "max_objects_in_get" limit.
/// ///
ids: Option<JmapArgument<Vec<String>>> ids: Option<JmapArgument<Vec<Id<OBJ>>>>
); );
_impl!( _impl!(
/// - properties: Option<Vec<String>> /// - properties: Option<Vec<String>>
@ -218,20 +394,19 @@ pub struct MethodResponse<'a> {
#[serde(borrow)] #[serde(borrow)]
pub method_responses: Vec<&'a RawValue>, pub method_responses: Vec<&'a RawValue>,
#[serde(default)] #[serde(default)]
pub created_ids: HashMap<Id, Id>, pub created_ids: HashMap<Id<String>, Id<String>>,
#[serde(default)] #[serde(default)]
pub session_state: String, pub session_state: State<JmapSession>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetResponse<OBJ: Object> { pub struct GetResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")] pub account_id: Id<Account>,
pub account_id: String, #[serde(default = "State::default")]
#[serde(default)] pub state: State<OBJ>,
pub state: String,
pub list: Vec<OBJ>, pub list: Vec<OBJ>,
pub not_found: Vec<String>, pub not_found: Vec<Id<OBJ>>,
} }
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> { impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
@ -244,10 +419,10 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetRes
} }
impl<OBJ: Object> GetResponse<OBJ> { impl<OBJ: Object> GetResponse<OBJ> {
_impl!(get_mut account_id_mut, account_id: String); _impl!(get_mut account_id_mut, account_id: Id<Account>);
_impl!(get_mut state_mut, state: String); _impl!(get_mut state_mut, state: State<OBJ>);
_impl!(get_mut list_mut, list: Vec<OBJ>); _impl!(get_mut list_mut, list: Vec<OBJ>);
_impl!(get_mut not_found_mut, not_found: Vec<String>); _impl!(get_mut not_found_mut, not_found: Vec<Id<OBJ>>);
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -264,7 +439,7 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where where
OBJ: std::fmt::Debug + Serialize, OBJ: std::fmt::Debug + Serialize,
{ {
account_id: String, account_id: Id<Account>,
filter: Option<F>, filter: Option<F>,
sort: Option<Comparator<OBJ>>, sort: Option<Comparator<OBJ>>,
#[serde(default)] #[serde(default)]
@ -288,7 +463,7 @@ where
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
account_id: String::new(), account_id: Id::new(),
filter: None, filter: None,
sort: None, sort: None,
position: 0, position: 0,
@ -300,7 +475,7 @@ where
} }
} }
_impl!(account_id: String); _impl!(account_id: Id<Account>);
_impl!(filter: Option<F>); _impl!(filter: Option<F>);
_impl!(sort: Option<Comparator<OBJ>>); _impl!(sort: Option<Comparator<OBJ>>);
_impl!(position: u64); _impl!(position: u64);
@ -325,12 +500,11 @@ pub fn bool_true() -> bool {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct QueryResponse<OBJ: Object> { pub struct QueryResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty", default)] pub account_id: Id<Account>,
pub account_id: String,
pub query_state: String, pub query_state: String,
pub can_calculate_changes: bool, pub can_calculate_changes: bool,
pub position: u64, pub position: u64,
pub ids: Vec<Id>, pub ids: Vec<Id<OBJ>>,
#[serde(default)] #[serde(default)]
pub total: u64, pub total: u64,
#[serde(default)] #[serde(default)]
@ -349,7 +523,7 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryR
} }
impl<OBJ: Object> QueryResponse<OBJ> { impl<OBJ: Object> QueryResponse<OBJ> {
_impl!(get_mut ids_mut, ids: Vec<Id>); _impl!(get_mut ids_mut, ids: Vec<Id<OBJ>>);
} }
pub struct ResultField<M: Method<OBJ>, OBJ: Object> { pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
@ -410,9 +584,8 @@ pub struct Changes<OBJ: Object>
where where
OBJ: std::fmt::Debug + Serialize, OBJ: std::fmt::Debug + Serialize,
{ {
#[serde(skip_serializing_if = "String::is_empty")] pub account_id: Id<Account>,
pub account_id: String, pub since_state: State<OBJ>,
pub since_state: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_changes: Option<u64>, pub max_changes: Option<u64>,
#[serde(skip)] #[serde(skip)]
@ -425,8 +598,8 @@ where
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
account_id: String::new(), account_id: Id::new(),
since_state: String::new(), since_state: State::new(),
max_changes: None, max_changes: None,
_ph: PhantomData, _ph: PhantomData,
} }
@ -436,7 +609,7 @@ where
/// ///
/// The id of the account to use. /// The id of the account to use.
/// ///
account_id: String account_id: Id<Account>
); );
_impl!( _impl!(
/// - since_state: "String" /// - since_state: "String"
@ -446,7 +619,7 @@ where
/// state. /// state.
/// ///
/// ///
since_state: String since_state: State<OBJ>
); );
_impl!( _impl!(
/// - max_changes: "UnsignedInt|null" /// - max_changes: "UnsignedInt|null"
@ -463,14 +636,13 @@ where
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChangesResponse<OBJ: Object> { pub struct ChangesResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")] pub account_id: Id<Account>,
pub account_id: String, pub old_state: State<OBJ>,
pub old_state: String, pub new_state: State<OBJ>,
pub new_state: String,
pub has_more_changes: bool, pub has_more_changes: bool,
pub created: Vec<Id>, pub created: Vec<Id<OBJ>>,
pub updated: Vec<Id>, pub updated: Vec<Id<OBJ>>,
pub destroyed: Vec<Id>, pub destroyed: Vec<Id<OBJ>>,
#[serde(skip)] #[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>, pub _ph: PhantomData<fn() -> OBJ>,
} }
@ -485,13 +657,13 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for Change
} }
impl<OBJ: Object> ChangesResponse<OBJ> { impl<OBJ: Object> ChangesResponse<OBJ> {
_impl!(get_mut account_id_mut, account_id: String); _impl!(get_mut account_id_mut, account_id: Id<Account>);
_impl!(get_mut old_state_mut, old_state: String); _impl!(get_mut old_state_mut, old_state: State<OBJ>);
_impl!(get_mut new_state_mut, new_state: String); _impl!(get_mut new_state_mut, new_state: State<OBJ>);
_impl!(get has_more_changes, has_more_changes: bool); _impl!(get has_more_changes, has_more_changes: bool);
_impl!(get_mut created_mut, created: Vec<String>); _impl!(get_mut created_mut, created: Vec<Id<OBJ>>);
_impl!(get_mut updated_mut, updated: Vec<String>); _impl!(get_mut updated_mut, updated: Vec<Id<OBJ>>);
_impl!(get_mut destroyed_mut, destroyed: Vec<String>); _impl!(get_mut destroyed_mut, destroyed: Vec<Id<OBJ>>);
} }
///#`set` ///#`set`
@ -508,11 +680,10 @@ pub struct Set<OBJ: Object>
where where
OBJ: std::fmt::Debug + Serialize, OBJ: std::fmt::Debug + Serialize,
{ {
#[serde(skip_serializing_if = "String::is_empty")]
///o accountId: "Id" ///o accountId: "Id"
/// ///
/// The id of the account to use. /// The id of the account to use.
pub account_id: String, pub account_id: Id<Account>,
///o ifInState: "String|null" ///o ifInState: "String|null"
/// ///
/// This is a state string as returned by the "Foo/get" method /// 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 /// otherwise, the method will be aborted and a "stateMismatch" error
/// returned. If null, any changes will be applied to the current /// returned. If null, any changes will be applied to the current
/// state. /// state.
pub if_in_state: Option<String>, pub if_in_state: Option<State<OBJ>>,
///o create: "Id[Foo]|null" ///o create: "Id[Foo]|null"
/// ///
/// A map of a *creation id* (a temporary id set by the client) to Foo /// 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 /// The client MUST omit any properties that may only be set by the
/// server (for example, the "id" property on most object types). /// server (for example, the "id" property on most object types).
/// ///
pub create: Option<HashMap<Id, OBJ>>, pub create: Option<HashMap<Id<OBJ>, OBJ>>,
///o update: "Id[PatchObject]|null" ///o update: "Id[PatchObject]|null"
/// ///
/// A map of an id to a Patch object to apply to the current Foo /// 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 /// is also a valid PatchObject. The client may choose to optimise
/// network usage by just sending the diff or may send the whole /// network usage by just sending the diff or may send the whole
/// object; the server processes it the same either way. /// object; the server processes it the same either way.
pub update: Option<HashMap<Id, Value>>, pub update: Option<HashMap<Id<OBJ>, Value>>,
///o destroy: "Id[]|null" ///o destroy: "Id[]|null"
/// ///
/// A list of ids for Foo objects to permanently delete, or null if no /// A list of ids for Foo objects to permanently delete, or null if no
/// objects are to be destroyed. /// objects are to be destroyed.
pub destroy: Option<Vec<Id>>, pub destroy: Option<Vec<Id<OBJ>>>,
} }
impl<OBJ: Object> Set<OBJ> impl<OBJ: Object> Set<OBJ>
@ -591,14 +762,14 @@ where
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
account_id: String::new(), account_id: Id::new(),
if_in_state: None, if_in_state: None,
create: None, create: None,
update: None, update: None,
destroy: None, destroy: None,
} }
} }
_impl!(account_id: String); _impl!(account_id: Id<Account>);
_impl!( _impl!(
///o ifInState: "String|null" ///o ifInState: "String|null"
/// ///
@ -608,9 +779,9 @@ where
/// otherwise, the method will be aborted and a "stateMismatch" error /// otherwise, the method will be aborted and a "stateMismatch" error
/// returned. If null, any changes will be applied to the current /// returned. If null, any changes will be applied to the current
/// state. /// state.
if_in_state: Option<String> if_in_state: Option<State<OBJ>>
); );
_impl!(update: Option<HashMap<Id, Value>>); _impl!(update: Option<HashMap<Id<OBJ>, Value>>);
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -619,17 +790,17 @@ pub struct SetResponse<OBJ: Object> {
///o accountId: "Id" ///o accountId: "Id"
/// ///
/// The id of the account used for the call. /// The id of the account used for the call.
pub account_id: String, pub account_id: Id<Account>,
///o oldState: "String|null" ///o oldState: "String|null"
/// ///
/// The state string that would have been returned by "Foo/get" before /// The state string that would have been returned by "Foo/get" before
/// making the requested changes, or null if the server doesn't know /// making the requested changes, or null if the server doesn't know
/// what the previous state string was. /// what the previous state string was.
pub old_state: String, pub old_state: State<OBJ>,
///o newState: "String" ///o newState: "String"
/// ///
/// The state string that will now be returned by "Foo/get". /// The state string that will now be returned by "Foo/get".
pub new_state: String, pub new_state: State<OBJ>,
///o created: "Id[Foo]|null" ///o created: "Id[Foo]|null"
/// ///
/// A map of the creation id to an object containing any properties of /// A map of the creation id to an object containing any properties of
@ -639,7 +810,7 @@ pub struct SetResponse<OBJ: Object> {
/// and thus set to a default by the server. /// and thus set to a default by the server.
/// ///
/// This argument is null if no Foo objects were successfully created. /// This argument is null if no Foo objects were successfully created.
pub created: Option<HashMap<Id, OBJ>>, pub created: Option<HashMap<Id<OBJ>, OBJ>>,
///o updated: "Id[Foo|null]|null" ///o updated: "Id[Foo|null]|null"
/// ///
/// The keys in this map are the ids of all Foos that were /// The keys in this map are the ids of all Foos that were
@ -651,12 +822,12 @@ pub struct SetResponse<OBJ: Object> {
/// any changes to server-set or computed properties. /// any changes to server-set or computed properties.
/// ///
/// This argument is null if no Foo objects were successfully updated. /// This argument is null if no Foo objects were successfully updated.
pub updated: Option<HashMap<Id, Option<OBJ>>>, pub updated: Option<HashMap<Id<OBJ>, Option<OBJ>>>,
///o destroyed: "Id[]|null" ///o destroyed: "Id[]|null"
/// ///
/// A list of Foo ids for records that were successfully destroyed, or /// A list of Foo ids for records that were successfully destroyed, or
/// null if none. /// null if none.
pub destroyed: Option<Vec<Id>>, pub destroyed: Option<Vec<Id<OBJ>>>,
///o notCreated: "Id[SetError]|null" ///o notCreated: "Id[SetError]|null"
/// ///
/// A map of the creation id to a SetError object for each record that /// 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( pub fn download_request_format(
session: &JmapSession, session: &JmapSession,
account_id: &Id, account_id: &Id<Account>,
blob_id: &Id, blob_id: &Id<BlobObject>,
name: Option<String>, name: Option<String>,
) -> String { ) -> String {
// https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} // 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]); ret.push_str(&session.download_url[prev_pos..prev_pos + pos]);
prev_pos += pos; prev_pos += pos;
if session.download_url[prev_pos..].starts_with("{accountId}") { 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(); prev_pos += "{accountId}".len();
} else if session.download_url[prev_pos..].starts_with("{blobId}") { } 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(); prev_pos += "{blobId}".len();
} else if session.download_url[prev_pos..].starts_with("{name}") { } else if session.download_url[prev_pos..].starts_with("{name}") {
ret.push_str(name.as_ref().map(String::as_str).unwrap_or("")); ret.push_str(name.as_ref().map(String::as_str).unwrap_or(""));
@ -793,7 +964,7 @@ pub fn download_request_format(
ret ret
} }
pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { pub fn upload_request_format(session: &JmapSession, account_id: &Id<Account>) -> String {
//"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", //"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/",
let mut ret = String::with_capacity(session.upload_url.len() + account_id.len()); let mut ret = String::with_capacity(session.upload_url.len() + account_id.len());
let mut prev_pos = 0; 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]); ret.push_str(&session.upload_url[prev_pos..prev_pos + pos]);
prev_pos += pos; prev_pos += pos;
if session.upload_url[prev_pos..].starts_with("{accountId}") { 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(); prev_pos += "{accountId}".len();
break; break;
} else { } else {
@ -822,12 +993,12 @@ pub struct UploadResponse {
///o accountId: "Id" ///o accountId: "Id"
/// ///
/// The id of the account used for the call. /// The id of the account used for the call.
pub account_id: String, pub account_id: Id<Account>,
///o blobId: "Id" ///o blobId: "Id"
/// ///
///The id representing the binary data uploaded. The data for this id is immutable. ///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. ///The id *only* refers to the binary data, not any metadata.
pub blob_id: String, pub blob_id: Id<BlobObject>,
///o type: "String" ///o type: "String"
/// ///
///The media type of the file (as specified in [RFC6838], ///The media type of the file (as specified in [RFC6838],