From a1efeed34352e60434e511edc5d35ffb4c6b4276 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 4 Dec 2019 01:04:38 +0200 Subject: [PATCH] JMAP WIP #3 --- melib/src/backends/jmap.rs | 72 +++------- melib/src/backends/jmap/connection.rs | 64 +++++++++ melib/src/backends/jmap/objects/email.rs | 64 ++++++++- melib/src/backends/jmap/protocol.rs | 134 ++++++++++++------ melib/src/backends/jmap/rfc8620.rs | 105 ++++++++++++-- melib/src/backends/jmap/rfc8620/comparator.rs | 17 +++ 6 files changed, 344 insertions(+), 112 deletions(-) create mode 100644 melib/src/backends/jmap/connection.rs diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index 26d850d0..8d25e982 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -35,6 +35,19 @@ use std::hash::Hasher; use std::str::FromStr; use std::sync::{Arc, Mutex, RwLock}; +#[macro_export] +macro_rules! _impl { + ($field:ident : $t:ty) => { + pub fn $field(mut self, new_val: $t) -> Self { + self.$field = new_val; + self + } + } + } + +pub mod connection; +use connection::*; + pub mod protocol; use protocol::*; @@ -122,7 +135,7 @@ macro_rules! get_conf_val { ($s:ident[$var:literal]) => { $s.extra.get($var).ok_or_else(|| { MeliError::new(format!( - "Configuration error ({}): IMAP connection requires the field `{}` set", + "Configuration error ({}): JMAP connection requires the field `{}` set", $s.name.as_str(), $var )) @@ -152,7 +165,7 @@ pub struct JmapType { online: Arc>, is_subscribed: Arc, server_conf: JmapServerConf, - connection: Arc>, + connection: Arc, folders: Arc>>, } @@ -168,15 +181,11 @@ impl MailBackend for JmapType { let handle = { let tx = w.tx(); let closure = move |_work_context| { - let mut conn_lck = connection.lock().unwrap(); tx.send(AsyncStatus::Payload( - protocol::get_message_list( - &mut conn_lck, - &folders.read().unwrap()[&folder_hash], - ) - .and_then(|ids| { - protocol::get_message(&mut conn_lck, std::dbg!(&ids).as_slice()) - }), + protocol::get_message_list(&connection, &folders.read().unwrap()[&folder_hash]) + .and_then(|ids| { + protocol::get_message(&connection, std::dbg!(&ids).as_slice()) + }), )) .unwrap(); tx.send(AsyncStatus::Finished).unwrap(); @@ -196,9 +205,7 @@ impl MailBackend for JmapType { fn folders(&self) -> Result> { if self.folders.read().unwrap().is_empty() { - let folders = std::dbg!(protocol::get_mailboxes( - &mut self.connection.lock().unwrap() - ))?; + let folders = std::dbg!(protocol::get_mailboxes(&self.connection))?; let ret = Ok(folders .iter() .map(|(&h, f)| (h, BackendFolder::clone(f) as Folder)) @@ -242,10 +249,7 @@ impl JmapType { let server_conf = JmapServerConf::new(s)?; Ok(Box::new(JmapType { - connection: Arc::new(Mutex::new(JmapConnection::new( - &server_conf, - online.clone(), - )?)), + connection: Arc::new(JmapConnection::new(&server_conf, online.clone())?), folders: Arc::new(RwLock::new(FnvHashMap::default())), account_name: s.name.clone(), online, @@ -263,37 +267,3 @@ impl JmapType { Ok(()) } } - -#[derive(Debug)] -pub struct JmapConnection { - request_no: usize, - client: Client, - online_status: Arc>, -} - -impl JmapConnection { - pub fn new(server_conf: &JmapServerConf, online_status: Arc>) -> Result { - use reqwest::header; - let mut headers = header::HeaderMap::new(); - headers.insert( - header::AUTHORIZATION, - header::HeaderValue::from_static("fc32dffe-14e7-11ea-a277-2477037a1804"), - ); - headers.insert( - header::ACCEPT, - header::HeaderValue::from_static("application/json"), - ); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ); - Ok(JmapConnection { - request_no: 0, - client: reqwest::blocking::ClientBuilder::new() - .danger_accept_invalid_certs(server_conf.danger_accept_invalid_certs) - .default_headers(headers) - .build()?, - online_status, - }) - } -} diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs new file mode 100644 index 00000000..036abd82 --- /dev/null +++ b/melib/src/backends/jmap/connection.rs @@ -0,0 +1,64 @@ +/* + * meli - jmap module. + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; + +#[derive(Debug)] +pub struct JmapConnection { + pub request_no: Arc>, + pub client: Arc>, + pub online_status: Arc>, + pub server_conf: JmapServerConf, +} + +impl JmapConnection { + pub fn new(server_conf: &JmapServerConf, online_status: Arc>) -> Result { + use reqwest::header; + let mut headers = header::HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_static("fc32dffe-14e7-11ea-a277-2477037a1804"), + ); + headers.insert( + header::ACCEPT, + header::HeaderValue::from_static("application/json"), + ); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + let client = reqwest::blocking::ClientBuilder::new() + .danger_accept_invalid_certs(server_conf.danger_accept_invalid_certs) + .default_headers(headers) + .build()?; + + let res_text = client.get(&server_conf.server_hostname).send()?.text()?; + debug!(&res_text); + + let server_conf = server_conf.clone(); + Ok(JmapConnection { + request_no: Arc::new(Mutex::new(0)), + client: Arc::new(Mutex::new(client)), + online_status, + server_conf, + }) + } +} diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index 851e41c0..d10d40e4 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -21,6 +21,7 @@ use super::*; use crate::backends::jmap::protocol::*; +use crate::backends::jmap::rfc8620::bool_false; use std::collections::HashMap; // 4.1.1. @@ -151,7 +152,7 @@ pub struct EmailQueryResponse { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct EmailQueryCall { - pub filter: Vec, /* "inMailboxes": [ folder.id ] },*/ + pub filter: EmailFilterCondition, /* "inMailboxes": [ folder.id ] },*/ pub collapse_threads: bool, pub position: u64, pub fetch_threads: bool, @@ -166,18 +167,44 @@ impl Method for EmailQueryCall { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct EmailGetCall { - pub filter: Vec, /* "inMailboxes": [ folder.id ] },*/ - pub collapse_threads: bool, - pub position: u64, - pub fetch_threads: bool, - pub fetch_messages: bool, - pub fetch_message_properties: Vec, + #[serde(flatten)] + pub get_call: GetCall, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub body_properties: Vec, + #[serde(default = "bool_false")] + pub fetch_text_body_values: bool, + #[serde(default = "bool_false")] + pub fetch_html_body_values: bool, + #[serde(default = "bool_false")] + pub fetch_all_body_values: bool, + #[serde(default)] + pub max_body_value_bytes: u64, } impl Method for EmailGetCall { const NAME: &'static str = "Email/get"; } +impl EmailGetCall { + pub fn new(get_call: GetCall) -> Self { + EmailGetCall { + get_call, + body_properties: Vec::new(), + fetch_text_body_values: false, + fetch_html_body_values: false, + fetch_all_body_values: false, + max_body_value_bytes: 0, + } + } + + _impl!(get_call: GetCall); + _impl!(body_properties: Vec); + _impl!(fetch_text_body_values: bool); + _impl!(fetch_html_body_values: bool); + _impl!(fetch_all_body_values: bool); + _impl!(max_body_value_bytes: u64); +} + #[derive(Serialize, Deserialize, Default, Debug)] #[serde(rename_all = "camelCase")] pub struct EmailFilterCondition { @@ -225,6 +252,29 @@ pub struct EmailFilterCondition { pub header: Vec, } +impl EmailFilterCondition { + _impl!(in_mailboxes: Vec); + _impl!(in_mailbox_other_than: Vec); + _impl!(before: UtcDate); + _impl!(after: UtcDate); + _impl!(min_size: Option); + _impl!(max_size: Option); + _impl!(all_in_thread_have_keyword: String); + _impl!(some_in_thread_have_keyword: String); + _impl!(none_in_thread_have_keyword: String); + _impl!(has_keyword: String); + _impl!(not_keyword: String); + _impl!(has_attachment: Option); + _impl!(text: String); + _impl!(from: String); + _impl!(to: String); + _impl!(cc: String); + _impl!(bcc: String); + _impl!(subject: String); + _impl!(body: String); + _impl!(header: Vec); +} + impl FilterTrait for EmailFilterCondition {} // The following convenience properties are also specified for the Email diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 125e9366..8557f253 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -29,6 +29,15 @@ pub type UtcDate = String; use super::rfc8620::Object; +macro_rules! get_request_no { + ($lock:expr) => {{ + let mut lck = $lock.lock().unwrap(); + let ret = *lck; + *lck += 1; + ret + }}; +} + pub trait Method: Serialize { const NAME: &'static str; } @@ -68,19 +77,25 @@ pub struct Request { /* Why is this Value instead of Box>? The Method trait cannot be made into a * Trait object because its serialize() will be generic. */ method_calls: Vec, + + #[serde(skip_serializing)] + request_no: Arc>, } impl Request { - pub fn new() -> Self { + pub fn new(request_no: Arc>) -> Self { Request { using: USING, method_calls: Vec::new(), + request_no, } } - pub fn add_call, O: Object>(&mut self, call: M) { + pub fn add_call, O: Object>(&mut self, call: M) -> usize { + let seq = get_request_no!(self.request_no); self.method_calls - .push(serde_json::to_value((M::NAME, call, "f")).unwrap()); + .push(serde_json::to_value((M::NAME, call, &format!("m{}", seq))).unwrap()); + seq } } @@ -100,17 +115,19 @@ pub enum MethodCall { Empty {}, } -pub fn get_mailboxes(conn: &mut JmapConnection) -> Result> { +pub fn get_mailboxes(conn: &JmapConnection) -> Result> { + let seq = get_request_no!(conn.request_no); let res = conn .client + .lock() + .unwrap() .post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/") .json(&json!({ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [["Mailbox/get", {}, - format!("#m{}", conn.request_no + 1).as_str()]], + format!("#m{}",seq).as_str()]], })) .send(); - conn.request_no += 1; let mut v: JsonResponse = serde_json::from_str(&std::dbg!(res.unwrap().text().unwrap())).unwrap(); @@ -259,12 +276,13 @@ pub struct JmapRights { // fetchSearchSnippets: false // }, "call1"] // ] -pub fn get_message_list(conn: &mut JmapConnection, folder: &JmapFolder) -> Result> { +pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result> { + let seq = get_request_no!(conn.request_no); let email_call: EmailQueryCall = EmailQueryCall { - filter: vec![EmailFilterCondition { + filter: EmailFilterCondition { in_mailboxes: vec![folder.id.clone()], ..Default::default() - }], + }, collapse_threads: false, position: 0, fetch_threads: true, @@ -285,9 +303,8 @@ pub fn get_message_list(conn: &mut JmapConnection, folder: &JmapFolder) -> Resul ], }; - let mut req = Request::new(); + let mut req = Request::new(conn.request_no.clone()); req.add_call(email_call); - std::dbg!(serde_json::to_string(&req)); /* { @@ -328,37 +345,43 @@ pub fn get_message_list(conn: &mut JmapConnection, folder: &JmapFolder) -> Resul ]] } */ + /* + r#" + "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], + "methodCalls": [["Email/query", { "filter": { + "inMailboxes": [ folder.id ] + }, + "collapseThreads": false, + "position": 0, + "fetchThreads": true, + "fetchMessages": true, + "fetchMessageProperties": [ + "threadId", + "mailboxId", + "isUnread", + "isFlagged", + "isAnswered", + "isDraft", + "hasAttachment", + "from", + "to", + "subject", + "date", + "preview" + ], + }, format!("m{}", seq).as_str()]], + });" + );*/ + + std::dbg!(serde_json::to_string(&req)); let res = conn .client + .lock() + .unwrap() .post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/") - .json(&json!({ - "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], - "methodCalls": [["Email/query", { "filter": { - "inMailboxes": [ folder.id ] - }, - "collapseThreads": false, - "position": 0, - "fetchThreads": true, - "fetchMessages": true, - "fetchMessageProperties": [ - "threadId", - "mailboxId", - "isUnread", - "isFlagged", - "isAnswered", - "isDraft", - "hasAttachment", - "from", - "to", - "subject", - "date", - "preview" - ], - }, format!("#m{}", conn.request_no + 1).as_str()]], - })) + .json(&req) .send(); - conn.request_no += 1; let mut v: JsonResponse = serde_json::from_str(&std::dbg!(res.unwrap().text().unwrap()))?; let result: Response = v.method_responses.remove(0).1; @@ -369,11 +392,35 @@ pub fn get_message_list(conn: &mut JmapConnection, folder: &JmapFolder) -> Resul } } -pub fn get_message(conn: &mut JmapConnection, ids: &[String]) -> Result> { +pub fn get_message(conn: &JmapConnection, ids: &[String]) -> Result> { + let seq = get_request_no!(conn.request_no); + let email_call: EmailGetCall = + EmailGetCall::new(GetCall::new().ids(Some(ids.iter().cloned().collect::>()))); + + let mut req = Request::new(conn.request_no.clone()); + req.add_call(email_call); let res = conn .client + .lock() + .unwrap() .post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/") - .json(&json!({ + .json(&req) + .send(); + + let res_text = res?.text()?; + let v: JsonResponse = serde_json::from_str(&res_text)?; + let mut f = std::fs::File::create(std::dbg!(format!("/tmp/asdfsa{}", seq))).unwrap(); + use std::io::Write; + f.write_all( + serde_json::to_string_pretty(&serde_json::from_str::(&res_text)?)?.as_bytes(), + ) + .unwrap(); + Ok(vec![]) +} + +/* + * + *json!({ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [["Email/get", { "ids": ids, @@ -383,12 +430,7 @@ pub fn get_message(conn: &mut JmapConnection, ids: &[String]) -> Result. */ +use super::Id; use core::marker::PhantomData; use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; + mod filters; pub use filters::*; mod comparator; pub use comparator::*; use super::protocol::Method; +use std::collections::HashMap; pub trait Object {} // 5.1. /get @@ -57,20 +61,76 @@ pub trait Object {} #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct GetCall> +pub struct JmapSession { + capabilities: HashMap, + accounts: HashMap, + primary_accounts: Vec, + username: String, + api_url: String, + download_url: String, + + upload_url: String, + event_source_url: String, + state: String, + #[serde(flatten)] + extra_properties: HashMap, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CapabilitiesObject { + max_size_upload: u64, + max_concurrent_upload: u64, + max_size_request: u64, + max_concurrent_requests: u64, + max_calls_in_request: u64, + max_objects_in_get: u64, + max_objects_in_set: u64, + collation_algorithms: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Account { + name: String, + is_personal: bool, + is_read_only: bool, + account_capabilities: HashMap, + #[serde(flatten)] + extra_properties: HashMap, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetCall where OBJ: std::fmt::Debug + Serialize, { #[serde(skip_serializing_if = "String::is_empty")] - account_id: String, + pub account_id: String, #[serde(skip_serializing_if = "Option::is_none")] - ids: Option>, + pub ids: Option>, #[serde(skip_serializing_if = "Option::is_none")] - properties: Option>, - _ph: PhantomData<*const CALL>, - __ph: PhantomData<*const OBJ>, + pub properties: Option>, + _ph: PhantomData<*const OBJ>, } +impl GetCall +where + OBJ: std::fmt::Debug + Serialize, +{ + pub fn new() -> Self { + Self { + account_id: String::new(), + ids: None, + properties: None, + _ph: PhantomData, + } + } + _impl!(account_id: String); + _impl!(ids: Option>); + _impl!(properties: Option>); +} // The response has the following arguments: // // o accountId: "Id" @@ -128,6 +188,7 @@ pub struct GetResponse { enum JmapError { RequestTooLarge, InvalidArguments, + InvalidResultReference, } // 5.5. /query @@ -321,11 +382,39 @@ where _ph: PhantomData<*const OBJ>, } -fn bool_false() -> bool { +impl, OBJ: Object> QueryCall +where + OBJ: std::fmt::Debug + Serialize, +{ + pub fn new() -> Self { + Self { + account_id: String::new(), + filter: None, + sort: None, + position: 0, + anchor: None, + anchor_offset: 0, + limit: None, + calculate_total: false, + _ph: PhantomData, + } + } + + _impl!(account_id: String); + _impl!(filter: Option>); + _impl!(sort: Option>); + _impl!(position: u64); + _impl!(anchor: Option); + _impl!(anchor_offset: u64); + _impl!(limit: Option); + _impl!(calculate_total: bool); +} + +pub fn bool_false() -> bool { false } -fn bool_true() -> bool { +pub fn bool_true() -> bool { true } diff --git a/melib/src/backends/jmap/rfc8620/comparator.rs b/melib/src/backends/jmap/rfc8620/comparator.rs index c11ad266..e1bad283 100644 --- a/melib/src/backends/jmap/rfc8620/comparator.rs +++ b/melib/src/backends/jmap/rfc8620/comparator.rs @@ -35,6 +35,23 @@ pub struct Comparator { _ph: PhantomData<*const OBJ>, } +impl Comparator { + pub fn new() -> Self { + Self { + property: String::new(), + is_ascending: true, + collation: None, + additional_properties: Vec::new(), + _ph: PhantomData, + } + } + + _impl!(property: String); + _impl!(is_ascending: bool); + _impl!(collation: Option); + _impl!(additional_properties: Vec); +} + #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] pub enum FilterOperator {