Browse Source

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 1 year ago
parent
commit
425f4b9930
Signed by untrusted user: epilys GPG Key ID: 73627C2F690DF710
  1. 79
      melib/src/backends/jmap.rs
  2. 37
      melib/src/backends/jmap/connection.rs
  3. 7
      melib/src/backends/jmap/mailbox.rs
  4. 49
      melib/src/backends/jmap/objects/email.rs
  5. 42
      melib/src/backends/jmap/objects/email/import.rs
  6. 12
      melib/src/backends/jmap/objects/mailbox.rs
  7. 39
      melib/src/backends/jmap/protocol.rs
  8. 313
      melib/src/backends/jmap/rfc8620.rs

79
melib/src/backends/jmap.rs

@ -191,15 +191,16 @@ macro_rules! get_conf_val {
pub struct Store {
pub account_name: Arc<String>,
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 id_store: Arc<Mutex<HashMap<EnvelopeHash, Id>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id, EnvelopeHash>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id>>>,
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
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 is_subscribed: Arc<IsSubscribedFn>,
pub event_consumer: BackendEventConsumer,
@ -275,7 +276,7 @@ impl Store {
pub fn remove_envelope(
&self,
obj_id: Id,
obj_id: Id<EmailObject>,
) -> 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<MailboxObject> = {
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<EmailObject> = "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::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { 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<String, Value> = HashMap::default();
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default();
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = 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::<EmailObject>::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<String, Value> = HashMap::default();
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default();
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = 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::<EmailObject>::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::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { 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 {

37
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<Account> {
&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<EmailObject> = 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::<EmailObject>::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::<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);
@ -153,6 +147,12 @@ impl JmapConnection {
GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
debug!(&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]>> =
Vec::with_capacity(list.len());
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> {
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;
}
}

7
melib/src/backends/jmap/mailbox.rs

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

49
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<EmailObject> {
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<EmailObject>,
#[serde(default)]
pub blob_id: String,
pub blob_id: Id<BlobObject>,
#[serde(default)]
pub mailbox_ids: HashMap<Id, bool>,
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
#[serde(default)]
pub size: u64,
#[serde(default)]
@ -163,7 +178,7 @@ pub struct EmailObject {
#[serde(default)]
pub keywords: HashMap<String, bool>,
#[serde(default)]
pub attached_emails: Option<Id>,
pub attached_emails: Option<Id<BlobObject>>,
#[serde(default)]
pub attachments: Vec<Value>,
#[serde(default)]
@ -182,7 +197,7 @@ pub struct EmailObject {
#[serde(default)]
pub text_body: Vec<TextBody>,
#[serde(default)]
pub thread_id: Id,
pub thread_id: Id<ThreadObject>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
@ -313,9 +328,7 @@ impl std::convert::From<EmailObject> 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<EmailObject> for crate::Envelope {
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct HtmlBody {
pub blob_id: Id,
pub blob_id: Id<BlobObject>,
#[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<BlobObject>,
#[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<Account>,
pub can_calculate_changes: bool,
pub collapse_threads: bool,
// FIXME
pub filter: String,
pub ids: Vec<Id>,
pub ids: Vec<Id<EmailObject>>,
pub position: u64,
pub query_state: String,
pub sort: Option<String>,
@ -469,9 +482,9 @@ impl EmailGet {
#[serde(rename_all = "camelCase")]
pub struct EmailFilterCondition {
#[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")]
pub in_mailbox_other_than: Vec<Id>,
pub in_mailbox_other_than: Vec<Id<MailboxObject>>,
#[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<Id>);
_impl!(in_mailbox_other_than: Vec<Id>);
_impl!(in_mailbox: Option<Id<MailboxObject>>);
_impl!(in_mailbox_other_than: Vec<Id<MailboxObject>>);
_impl!(before: UtcDate);
_impl!(after: UtcDate);
_impl!(min_size: Option<u64>);
@ -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),
)

42
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<Account>,
///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<String>,
pub if_in_state: Option<State<EmailObject>>,
///o emails: "Id[EmailImport]"
///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)]
@ -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<BlobObject>,
///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<Id, bool>,
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
///o keywords: "String[Boolean]" (default: {})
///The keywords to apply to the Email.
pub keywords: HashMap<String, bool>,
@ -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<Account>
);
_impl!(if_in_state: Option<String>);
_impl!(emails: HashMap<Id, EmailImport>);
_impl!(if_in_state: Option<State<EmailObject>>);
_impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
}
impl Method<EmailObject> for ImportCall {
@ -98,15 +98,15 @@ impl Method<EmailObject> 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<Id, bool>);
_impl!(blob_id: Id<BlobObject>);
_impl!(mailbox_ids: HashMap<Id<MailboxObject>, bool>);
_impl!(keywords: HashMap<String, bool>);
_impl!(received_at: Option<String>);
}
@ -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<EmailObject>,
},
///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<Account>,
///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<String>,
pub old_state: Option<State<EmailObject>>,
///o newState: "String"
///The state string that will now be returned by "Email/get" on this
///account.
pub new_state: Option<String>,
pub new_state: Option<State<EmailObject>>,
///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<Id, ImportEmailResult>,
pub created: HashMap<Id<EmailObject>, 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<Id, ImportError>,
pub not_created: HashMap<Id<EmailObject>, 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<EmailObject>,
pub blob_id: Id<BlobObject>,
pub thread_id: Id<ThreadObject>,
pub size: usize,
}

12
melib/src/backends/jmap/objects/mailbox.rs

@ -21,14 +21,22 @@
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)]
#[serde(rename_all = "camelCase")]
pub struct MailboxObject {
pub id: String,
pub id: Id<MailboxObject>,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub name: String,
pub parent_id: Option<String>,
pub parent_id: Option<Id<MailboxObject>>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: u64,

39
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<HashMap<MailboxHash,
unread_emails,
unread_threads,
} = 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,
JmapMailbox {
@ -137,6 +137,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
is_subscribed,
my_rights,
parent_id,
parent_hash,
role,
usage: Default::default(),
sort_order,
@ -150,10 +151,13 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
.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(
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::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { 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());

313
melib/src/backends/jmap/rfc8620.rs

@ -19,12 +19,12 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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<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)]
#[serde(rename_all = "camelCase")]
pub struct JmapSession {
pub capabilities: HashMap<String, CapabilitiesObject>,
pub accounts: HashMap<Id, Account>,
pub primary_accounts: HashMap<String, Id>,
pub accounts: HashMap<Id<Account>, Account>,
pub primary_accounts: HashMap<String, Id<Account>>,
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<JmapSession>,
#[serde(flatten)]
pub extra_properties: HashMap<String, Value>,
}
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<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`
///
/// Objects of type `Foo` are fetched via a call to `Foo/get`.
@ -104,11 +281,10 @@ pub struct Get<OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
pub account_id: Id<Account>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub ids: Option<JmapArgument<Vec<String>>>,
pub ids: Option<JmapArgument<Vec<Id<OBJ>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<String>>,
#[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<Account>
);
_impl!(
/// - ids: `Option<JmapArgument<Vec<String>>>`
@ -142,7 +318,7 @@ where
/// type and the number of records does not exceed the
/// "max_objects_in_get" limit.
///
ids: Option<JmapArgument<Vec<String>>>
ids: Option<JmapArgument<Vec<Id<OBJ>>>>
);
_impl!(
/// - properties: Option<Vec<String>>
@ -218,20 +394,19 @@ pub struct MethodResponse<'a> {
#[serde(borrow)]
pub method_responses: Vec<&'a RawValue>,
#[serde(default)]
pub created_ids: HashMap<Id, Id>,
pub created_ids: HashMap<Id<String>, Id<String>>,
#[serde(default)]
pub session_state: String,
pub session_state: State<JmapSession>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
#[serde(default)]
pub state: String,
pub account_id: Id<Account>,
#[serde(default = "State::default")]
pub state: State<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> {
@ -244,10 +419,10 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetRes
}
impl<OBJ: Object> GetResponse<OBJ> {
_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<Account>);
_impl!(get_mut state_mut, state: State<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)]
@ -264,7 +439,7 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
account_id: String,
account_id: Id<Account>,
filter: Option<F>,
sort: Option<Comparator<OBJ>>,
#[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<Account>);
_impl!(filter: Option<F>);
_impl!(sort: Option<Comparator<OBJ>>);
_impl!(position: u64);
@ -325,12 +500,11 @@ pub fn bool_true() -> bool {
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty", default)]
pub account_id: String,
pub account_id: Id<Account>,
pub query_state: String,
pub can_calculate_changes: bool,
pub position: u64,
pub ids: Vec<Id>,
pub ids: Vec<Id<OBJ>>,
#[serde(default)]
pub total: u64,
#[serde(default)]
@ -349,7 +523,7 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryR
}
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> {
@ -410,9 +584,8 @@ pub struct Changes<OBJ: Object>
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<Account>,
pub since_state: State<OBJ>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_changes: Option<u64>,
#[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<Account>
);
_impl!(
/// - since_state: "String"
@ -446,7 +619,7 @@ where
/// state.
///
///
since_state: String
since_state: State<OBJ>
);
_impl!(
/// - max_changes: "UnsignedInt|null"
@ -463,14 +636,13 @@ where
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChangesResponse<OBJ: Object> {
#[serde(skip_serializing_if = "String::is_empty")]
pub account_id: String,
pub old_state: String,
pub new_state: String,
pub account_id: Id<Account>,
pub old_state: State<OBJ>,
pub new_state: State<OBJ>,
pub has_more_changes: bool,
pub created: Vec<Id>,
pub updated: Vec<Id>,
pub destroyed: Vec<Id>,
pub created: Vec<Id<OBJ>>,
pub updated: Vec<Id<OBJ>>,
pub destroyed: Vec<Id<OBJ>>,
#[serde(skip)]
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!(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<Account>);
_impl!(get_mut old_state_mut, old_state: State<OBJ>);
_impl!(get_mut new_state_mut, new_state: State<OBJ>);
_impl!(get has_more_changes, has_more_changes: bool);
_impl!(get_mut created_mut, created: Vec<String>);
_impl!(get_mut updated_mut, updated: Vec<String>);
_impl!(get_mut destroyed_mut, destroyed: Vec<String>);
_impl!(get_mut created_mut, created: Vec<Id<OBJ>>);
_impl!(get_mut updated_mut, updated: Vec<Id<OBJ>>);
_impl!(get_mut destroyed_mut, destroyed: Vec<Id<OBJ>>);
}
///#`set`
@ -508,11 +680,10 @@ pub struct Set<OBJ: Object>
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<Account>,
///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<String>,
pub if_in_state: Option<State<OBJ>>,
///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<HashMap<Id, OBJ>>,
pub create: Option<HashMap<Id<OBJ>, 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<HashMap<Id, Value>>,
pub update: Option<HashMap<Id<OBJ>, 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<Vec<Id>>,
pub destroy: Option<Vec<Id<OBJ>>>,
}
impl<OBJ: Object> Set<OBJ>
@ -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<Account>);
_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<String>
if_in_state: Option<State<OBJ>>
);
_impl!(update: Option<HashMap<Id, Value>>);
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
}
#[derive(Serialize, Deserialize, Debug)]
@ -619,17 +790,17 @@ pub struct SetResponse<OBJ: Object> {
///o accountId: "Id"
///
/// The id of the account used for the call.
pub account_id: String,
pub account_id: Id<Account>,
///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<OBJ>,
///o newState: "String"
///
/// 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"
///
/// 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.
///
/// 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"
///
/// 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.
///
/// 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"
///
/// A list of Foo ids for records that were successfully destroyed, or
/// null if none.
pub destroyed: Option<Vec<Id