diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index c3b1832d4..2770bcd04 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -37,20 +37,24 @@ use std::sync::{Arc, Mutex, RwLock}; #[macro_export] macro_rules! _impl { - ($field:ident : $t:ty) => { + ($(#[$outer:meta])*$field:ident : $t:ty) => { + $(#[$outer])* pub fn $field(mut self, new_val: $t) -> Self { self.$field = new_val; self } - } - } - -#[macro_export] -macro_rules! _impl_get_mut { - ($method:ident, $field:ident : $t:ty) => { + }; + (get_mut $(#[$outer:meta])*$method:ident, $field:ident : $t:ty) => { + $(#[$outer])* pub fn $method(&mut self) -> &mut $t { &mut self.$field } + }; + (get $(#[$outer:meta])*$method:ident, $field:ident : $t:ty) => { + $(#[$outer])* + pub fn $method(&self) -> &$t { + &self.$field + } } } diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs index 8a40bb8dd..66e91ca56 100644 --- a/melib/src/backends/jmap/connection.rs +++ b/melib/src/backends/jmap/connection.rs @@ -28,6 +28,7 @@ pub struct JmapConnection { pub online_status: Arc>, pub server_conf: JmapServerConf, pub account_id: Arc>, + pub method_call_states: Arc>>, } impl JmapConnection { @@ -61,6 +62,7 @@ impl JmapConnection { online_status, server_conf, account_id: Arc::new(Mutex::new(String::new())), + method_call_states: Arc::new(Mutex::new(Default::default())), }) } } diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index 8340ca5ba..b1e437d39 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -22,6 +22,7 @@ use super::*; use crate::backends::jmap::protocol::*; use crate::backends::jmap::rfc8620::bool_false; +use core::marker::PhantomData; use serde::de::{Deserialize, Deserializer}; use serde_json::Value; use std::collections::hash_map::DefaultHasher; @@ -131,6 +132,8 @@ pub struct EmailObject { #[serde(default)] pub id: Id, #[serde(default)] + pub blob_id: String, + #[serde(default)] mailbox_ids: HashMap, #[serde(default)] size: u64, @@ -155,8 +158,6 @@ pub struct EmailObject { #[serde(default)] attachments: Vec, #[serde(default)] - pub blob_id: String, - #[serde(default)] has_attachment: bool, #[serde(default)] #[serde(deserialize_with = "deserialize_header")] @@ -366,6 +367,14 @@ impl Method for EmailQueryCall { const NAME: &'static str = "Email/query"; } +impl EmailQueryCall { + pub const RESULT_FIELD_IDS: ResultField = + ResultField:: { + field: "/ids", + _ph: PhantomData, + }; +} + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct EmailGet { @@ -486,7 +495,10 @@ impl FilterTrait for EmailFilterCondition {} #[serde(rename_all = "camelCase")] pub enum MessageProperty { ThreadId, - MailboxId, + MailboxIds, + Keywords, + Size, + ReceivedAt, IsUnread, IsFlagged, IsAnswered, @@ -494,7 +506,15 @@ pub enum MessageProperty { HasAttachment, From, To, + Cc, + Bcc, + ReplyTo, Subject, - Date, + SentAt, Preview, + Id, + BlobId, + MessageId, + InReplyTo, + Sender, } diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 99f1b8200..4e448d587 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -182,92 +182,12 @@ pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result::try_from(v.method_responses.pop().unwrap())?; - let GetResponse:: { list, .. } = e; + let GetResponse:: { list, state, .. } = e; + { + let mut states_lck = conn.method_call_states.lock().unwrap(); + + if let Some(prev_state) = states_lck.get_mut(&EmailGet::NAME) { + debug!("{:?}: prev_state was {}", EmailGet::NAME, prev_state); + + if *prev_state != state { /* Query Changes. */ } + + *prev_state = state; + debug!("{:?}: curr state is {}", EmailGet::NAME, prev_state); + } else { + debug!("{:?}: inserting state {}", EmailGet::NAME, &state); + states_lck.insert(EmailGet::NAME, state); + } + } let ids = list .iter() .map(|obj| (obj.id.clone(), obj.blob_id.clone())) diff --git a/melib/src/backends/jmap/rfc8620.rs b/melib/src/backends/jmap/rfc8620.rs index 3bdac1bc9..e68241a6d 100644 --- a/melib/src/backends/jmap/rfc8620.rs +++ b/melib/src/backends/jmap/rfc8620.rs @@ -38,32 +38,6 @@ pub trait Object { const NAME: &'static str; } -// 5.1. /get -// -// Objects of type Foo are fetched via a call to "Foo/get". -// -// It takes the following arguments: -// -// o accountId: "Id" -// -// The id of the account to use. -// -// o ids: "Id[]|null" -// -// The ids of the Foo objects to return. If null, then *all* records -// of the data type are returned, if this is supported for that data -// type and the number of records does not exceed the -// "maxObjectsInGet" limit. -// -// o properties: "String[]|null" -// -// If supplied, only the properties listed in the array are returned -// for each Foo object. If null, all properties of the object are -// returned. The id property of the object is *always* returned, -// even if not explicitly requested. If an invalid property is -// requested, the call MUST be rejected with an "invalidArguments" -// error. - #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct JmapSession { @@ -105,6 +79,16 @@ pub struct Account { extra_properties: HashMap, } +/// #`get` +/// +/// Objects of type `Foo` are fetched via a call to `Foo/get`. +/// +/// It takes the following arguments: +/// +/// - `account_id`: "Id" +/// +/// The id of the account to use. +/// #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Get @@ -134,9 +118,34 @@ where _ph: PhantomData, } } - _impl!(account_id: String); - _impl!(ids: Option>>); - _impl!(properties: Option>); + _impl!( + /// - accountId: "Id" + /// + /// The id of the account to use. + /// + account_id: String + ); + _impl!( + /// - ids: `Option>>` + /// + /// The ids of the Foo objects to return. If `None`, then *all* records + /// of the data type are returned, if this is supported for that data + /// type and the number of records does not exceed the + /// "max_objects_in_get" limit. + /// + ids: Option>> + ); + _impl!( + /// - properties: Option> + /// + /// If supplied, only the properties listed in the array are returned + /// for each `Foo` object. If `None`, all properties of the object are + /// returned. The `id` property of the object is *always* returned, + /// even if not explicitly requested. If an invalid property is + /// requested, the call WILL be rejected with an "invalid_arguments" + /// error. + properties: Option> + ); } impl Serialize for Get { @@ -194,48 +203,6 @@ impl Serialize for Get { } } -// The response has the following arguments: -// -// o accountId: "Id" -// -// The id of the account used for the call. -// -// o state: "String" -// -// A (preferably short) string representing the state on the server -// for *all* the data of this type in the account (not just the -// objects returned in this call). If the data changes, this string -// MUST change. If the Foo data is unchanged, servers SHOULD return -// the same state string on subsequent requests for this data type. -// When a client receives a response with a different state string to -// a previous call, it MUST either throw away all currently cached -// objects for the type or call "Foo/changes" to get the exact -// changes. -// -// o list: "Foo[]" -// -// An array of the Foo objects requested. This is the *empty array* -// if no objects were found or if the "ids" argument passed in was -// also an empty array. The results MAY be in a different order to -// the "ids" in the request arguments. If an identical id is -// included more than once in the request, the server MUST only -// include it once in either the "list" or the "notFound" argument of -// the response. -// -// o notFound: "Id[]" -// -// This array contains the ids passed to the method for records that -// do not exist. The array is empty if all requested ids were found -// or if the "ids" argument passed in was either null or an empty -// array. -// -// The following additional error may be returned instead of the "Foo/ -// get" response: -// -// "requestTooLarge": The number of ids requested by the client exceeds -// the maximum number the server is willing to process in a single -// method call. - #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MethodResponse<'a> { @@ -268,10 +235,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!(list_mut, list: Vec); - _impl_get_mut!(not_found_mut, not_found: Vec); + _impl!(get_mut account_id_mut, account_id: String); + _impl!(get_mut state_mut, state: String); + _impl!(get_mut list_mut, list: Vec); + _impl!(get_mut not_found_mut, not_found: Vec); } #[derive(Deserialize, Debug)] @@ -282,175 +249,6 @@ enum JmapError { InvalidResultReference, } -// 5.5. /query -// -// For data sets where the total amount of data is expected to be very -// small, clients can just fetch the complete set of data and then do -// any sorting/filtering locally. However, for large data sets (e.g., -// multi-gigabyte mailboxes), the client needs to be able to -// search/sort/window the data type on the server. -// -// A query on the set of Foos in an account is made by calling "Foo/ -// query". This takes a number of arguments to determine which records -// to include, how they should be sorted, and which part of the result -// should be returned (the full list may be *very* long). The result is -// returned as a list of Foo ids. -// -// A call to "Foo/query" takes the following arguments: -// -// o accountId: "Id" -// -// The id of the account to use. -// -// o filter: "FilterOperator|FilterCondition|null" -// -// Determines the set of Foos returned in the results. If null, all -// objects in the account of this type are included in the results. -// A *FilterOperator* object has the following properties: -// -// * operator: "String" -// -// This MUST be one of the following strings: -// -// + "AND": All of the conditions must match for the filter to -// match. -// -// + "OR": At least one of the conditions must match for the -// filter to match. -// -// + "NOT": None of the conditions must match for the filter to -// match. -// -// * conditions: "(FilterOperator|FilterCondition)[]" -// -// The conditions to evaluate against each record. -// -// A *FilterCondition* is an "object" whose allowed properties and -// semantics depend on the data type and is defined in the /query -// method specification for that type. It MUST NOT have an -// "operator" property. -// -// o sort: "Comparator[]|null" -// -// Lists the names of properties to compare between two Foo records, -// and how to compare them, to determine which comes first in the -// sort. If two Foo records have an identical value for the first -// comparator, the next comparator will be considered, and so on. If -// all comparators are the same (this includes the case where an -// empty array or null is given as the "sort" argument), the sort -// order is server dependent, but it MUST be stable between calls to -// "Foo/query". A *Comparator* has the following properties: -// -// * property: "String" -// -// The name of the property on the Foo objects to compare. -// -// * isAscending: "Boolean" (optional; default: true) -// -// If true, sort in ascending order. If false, reverse the -// comparator's results to sort in descending order. -// -// * collation: "String" (optional; default is server-dependent) -// -// The identifier, as registered in the collation registry defined -// in [RFC4790], for the algorithm to use when comparing the order -// of strings. The algorithms the server supports are advertised -// in the capabilities object returned with the Session object -// (see Section 2). -// -// If omitted, the default algorithm is server dependent, but: -// -// 1. It MUST be unicode-aware. -// -// 2. It MAY be selected based on an Accept-Language header in -// the request (as defined in [RFC7231], Section 5.3.5) or -// out-of-band information about the user's language/locale. -// -// 3. It SHOULD be case insensitive where such a concept makes -// sense for a language/locale. Where the user's language is -// unknown, it is RECOMMENDED to follow the advice in -// Section 5.2.3 of [RFC8264]. -// -// The "i;unicode-casemap" collation [RFC5051] and the Unicode -// Collation Algorithm () -// are two examples that fulfil these criterion and provide -// reasonable behaviour for a large number of languages. -// -// When the property being compared is not a string, the -// "collation" property is ignored, and the following comparison -// rules apply based on the type. In ascending order: -// -// + "Boolean": false comes before true. -// -// + "Number": A lower number comes before a higher number. -// -// + "Date"/"UTCDate": The earlier date comes first. -// -// The Comparator object may also have additional properties as -// required for specific sort operations defined in a type's /query -// method. -// -// o position: "Int" (default: 0) -// -// The zero-based index of the first id in the full list of results -// to return. -// -// If a negative value is given, it is an offset from the end of the -// list. Specifically, the negative value MUST be added to the total -// number of results given the filter, and if still negative, it's -// clamped to "0". This is now the zero-based index of the first id -// to return. -// -// If the index is greater than or equal to the total number of -// objects in the results list, then the "ids" array in the response -// will be empty, but this is not an error. -// -// o anchor: "Id|null" -// -// A Foo id. If supplied, the "position" argument is ignored. The -// index of this id in the results will be used in combination with -// the "anchorOffset" argument to determine the index of the first -// result to return (see below for more details). -// -// o anchorOffset: "Int" (default: 0) -// -// The index of the first result to return relative to the index of -// the anchor, if an anchor is given. This MAY be negative. For -// example, "-1" means the Foo immediately preceding the anchor is -// the first result in the list returned (see below for more -// details). -// -// o limit: "UnsignedInt|null" -// -// The maximum number of results to return. If null, no limit -// presumed. The server MAY choose to enforce a maximum "limit" -// argument. In this case, if a greater value is given (or if it is -// null), the limit is clamped to the maximum; the new limit is -// returned with the response so the client is aware. If a negative -// value is given, the call MUST be rejected with an -// "invalidArguments" error. -// -// o calculateTotal: "Boolean" (default: false) -// -// Does the client wish to know the total number of results in the -// query? This may be slow and expensive for servers to calculate, -// particularly with complex filters, so clients should take care to -// only request the total when needed. -// -// If an "anchor" argument is given, the anchor is looked for in the -// results after filtering and sorting. If found, the "anchorOffset" is -// then added to its index. If the resulting index is now negative, it -// is clamped to 0. This index is now used exactly as though it were -// supplied as the "position" argument. If the anchor is not found, the -// call is rejected with an "anchorNotFound" error. -// -// If an "anchor" is specified, any position argument supplied by the -// client MUST be ignored. If no "anchor" is supplied, any -// "anchorOffset" argument MUST be ignored. -// -// A client can use "anchor" instead of "position" to find the index of -// an id within a large set of results. - #[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryCall, OBJ: Object> @@ -510,84 +308,6 @@ pub fn bool_true() -> bool { true } -// The response has the following arguments: -// -// o accountId: "Id" -// -// The id of the account used for the call. -// -// o queryState: "String" -// -// A string encoding the current state of the query on the server. -// This string MUST change if the results of the query (i.e., the -// matching ids and their sort order) have changed. The queryState -// string MAY change if something has changed on the server, which -// means the results may have changed but the server doesn't know for -// sure. -// -// The queryState string only represents the ordered list of ids that -// match the particular query (including its sort/filter). There is -// no requirement for it to change if a property on an object -// matching the query changes but the query results are unaffected -// (indeed, it is more efficient if the queryState string does not -// change in this case). The queryState string only has meaning when -// compared to future responses to a query with the same type/sort/ -// filter or when used with /queryChanges to fetch changes. -// -// Should a client receive back a response with a different -// queryState string to a previous call, it MUST either throw away -// the currently cached query and fetch it again (note, this does not -// require fetching the records again, just the list of ids) or call -// "Foo/queryChanges" to get the difference. -// -// o canCalculateChanges: "Boolean" -// -// This is true if the server supports calling "Foo/queryChanges" -// with these "filter"/"sort" parameters. Note, this does not -// guarantee that the "Foo/queryChanges" call will succeed, as it may -// only be possible for a limited time afterwards due to server -// internal implementation details. -// -// o position: "UnsignedInt" -// -// The zero-based index of the first result in the "ids" array within -// the complete list of query results. -// -// o ids: "Id[]" -// -// The list of ids for each Foo in the query results, starting at the -// index given by the "position" argument of this response and -// continuing until it hits the end of the results or reaches the -// "limit" number of ids. If "position" is >= "total", this MUST be -// the empty list. -// -// o total: "UnsignedInt" (only if requested) -// -// The total number of Foos in the results (given the "filter"). -// This argument MUST be omitted if the "calculateTotal" request -// argument is not true. -// -// o limit: "UnsignedInt" (if set by the server) -// -// The limit enforced by the server on the maximum number of results -// to return. This is only returned if the server set a limit or -// used a different limit than that given in the request. -// -// The following additional errors may be returned instead of the "Foo/ -// query" response: -// -// "anchorNotFound": An anchor argument was supplied, but it cannot be -// found in the results of the query. -// -// "unsupportedSort": The "sort" is syntactically valid, but it includes -// a property the server does not support sorting on or a collation -// method it does not recognise. -// -// "unsupportedFilter": The "filter" is syntactically valid, but the -// server cannot process it. If the filter was the result of a user's -// search input, the client SHOULD suggest that the user simplify their -// search. - #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryResponse { @@ -615,5 +335,138 @@ 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> { + pub field: &'static str, + pub _ph: PhantomData<*const (OBJ, M)>, +} + +// error[E0723]: trait bounds other than `Sized` on const fn parameters are unstable +// --> melib/src/backends/jmap/rfc8620.rs:626:6 +// | +// 626 | impl, OBJ: Object> ResultField { +// | ^ +// | +// = note: for more information, see issue https://github.com/rust-lang/rust/issues/57563 +// = help: add `#![feature(const_fn)]` to the crate attributes to enable +// impl, OBJ: Object> ResultField { +// pub const fn new(field: &'static str) -> Self { +// Self { +// field, +// _ph: PhantomData, +// } +// } +// } + +/// #`changes` +/// +/// The "Foo/changes" method allows a client to efficiently update the state of its Foo cache +/// to match the new state on the server. It takes the following arguments: +/// +/// - accountId: "Id" The id of the account to use. +/// - sinceState: "String" +/// The current state of the client. This is the string that was +/// returned as the "state" argument in the "Foo/get" response. The +/// server will return the changes that have occurred since this +/// state. +/// +/// - maxChanges: "UnsignedInt|null" +/// The maximum number of ids to return in the response. The server +/// MAY choose to return fewer than this value but MUST NOT return +/// more. If not given by the client, the server may choose how many +/// to return. If supplied by the client, the value MUST be a +/// positive integer greater than 0. If a value outside of this range +/// is given, the server MUST re +/// +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +/* ch-ch-ch-ch-ch-Changes */ +pub struct Changes +where + OBJ: std::fmt::Debug + Serialize, +{ + #[serde(skip_serializing_if = "String::is_empty")] + pub account_id: String, + pub since_state: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_changes: Option, + #[serde(skip)] + _ph: PhantomData<*const OBJ>, +} + +impl Changes +where + OBJ: std::fmt::Debug + Serialize, +{ + pub fn new() -> Self { + Self { + account_id: String::new(), + since_state: String::new(), + max_changes: None, + _ph: PhantomData, + } + } + _impl!( + /// - accountId: "Id" + /// + /// The id of the account to use. + /// + account_id: String + ); + _impl!( + /// - since_state: "String" + /// The current state of the client. This is the string that was + /// returned as the "state" argument in the "Foo/get" response. The + /// server will return the changes that have occurred since this + /// state. + /// + /// + since_state: String + ); + _impl!( + /// - max_changes: "UnsignedInt|null" + /// The maximum number of ids to return in the response. The server + /// MAY choose to return fewer than this value but MUST NOT return + /// more. If not given by the client, the server may choose how many + /// to return. If supplied by the client, the value MUST be a + /// positive integer greater than 0. If a value outside of this range + /// is given, the server MUST re + max_changes: Option + ); +} + +#[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 has_more_changes: bool, + pub created: Vec, + pub updated: Vec, + pub destroyed: Vec, + #[serde(skip)] + _ph: PhantomData<*const OBJ>, +} + +impl std::convert::TryFrom<&RawValue> for ChangesResponse { + type Error = crate::error::MeliError; + fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { + let res: (String, ChangesResponse, String) = serde_json::from_str(t.get())?; + assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME)); + Ok(res.1) + } +} + +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 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); } diff --git a/melib/src/backends/jmap/rfc8620/argument.rs b/melib/src/backends/jmap/rfc8620/argument.rs index 498208fe4..5c8dccaef 100644 --- a/melib/src/backends/jmap/rfc8620/argument.rs +++ b/melib/src/backends/jmap/rfc8620/argument.rs @@ -21,6 +21,7 @@ use crate::backends::jmap::protocol::Method; use crate::backends::jmap::rfc8620::Object; +use crate::backends::jmap::rfc8620::ResultField; use serde::de::DeserializeOwned; use serde::ser::{Serialize, SerializeStruct, Serializer}; @@ -40,7 +41,7 @@ impl JmapArgument { JmapArgument::Value(v) } - pub fn reference(result_of: usize, method: &M, path: &str) -> Self + pub fn reference(result_of: usize, method: &M, path: ResultField) -> Self where M: Method, OBJ: Object, @@ -48,7 +49,7 @@ impl JmapArgument { JmapArgument::ResultReference { result_of: format!("m{}", result_of), name: M::NAME.to_string(), - path: path.to_string(), + path: path.field.to_string(), } } }