Browse Source

JMAP WIP #3

tags/alpha-0.6.0
Manos Pitsidianakis 1 year ago
parent
commit
a1efeed343
Signed by: epilys GPG Key ID: 73627C2F690DF710
6 changed files with 344 additions and 112 deletions
  1. +21
    -51
      melib/src/backends/jmap.rs
  2. +64
    -0
      melib/src/backends/jmap/connection.rs
  3. +57
    -7
      melib/src/backends/jmap/objects/email.rs
  4. +88
    -46
      melib/src/backends/jmap/protocol.rs
  5. +97
    -8
      melib/src/backends/jmap/rfc8620.rs
  6. +17
    -0
      melib/src/backends/jmap/rfc8620/comparator.rs

+ 21
- 51
melib/src/backends/jmap.rs View File

@ -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<Mutex<bool>>,
is_subscribed: Arc<IsSubscribedFn>,
server_conf: JmapServerConf,
connection: Arc<Mutex<JmapConnection>>,
connection: Arc<JmapConnection>,
folders: Arc<RwLock<FnvHashMap<FolderHash, JmapFolder>>>,
}
@ -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<FnvHashMap<FolderHash, Folder>> {
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<Mutex<bool>>,
}
impl JmapConnection {
pub fn new(server_conf: &JmapServerConf, online_status: Arc<Mutex<bool>>) -> Result<Self> {
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,
})
}
}

+ 64
- 0
melib/src/backends/jmap/connection.rs View File

@ -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 <http://www.gnu.org/licenses/>.
*/
use super::*;
#[derive(Debug)]
pub struct JmapConnection {
pub request_no: Arc<Mutex<usize>>,
pub client: Arc<Mutex<Client>>,
pub online_status: Arc<Mutex<bool>>,
pub server_conf: JmapServerConf,
}
impl JmapConnection {
pub fn new(server_conf: &JmapServerConf, online_status: Arc<Mutex<bool>>) -> Result<Self> {
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,
})
}
}

+ 57
- 7
melib/src/backends/jmap/objects/email.rs View File

@ -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<EmailFilterCondition>, /* "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<EmailFilterCondition>, /* "inMailboxes": [ folder.id ] },*/
pub collapse_threads: bool,
pub position: u64,
pub fetch_threads: bool,
pub fetch_messages: bool,
pub fetch_message_properties: Vec<MessageProperty>,
#[serde(flatten)]
pub get_call: GetCall<EmailObject>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub body_properties: Vec<String>,
#[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<EmailObject> for EmailGetCall {
const NAME: &'static str = "Email/get";
}
impl EmailGetCall {
pub fn new(get_call: GetCall<EmailObject>) -> 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<EmailObject>);
_impl!(body_properties: Vec<String>);
_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<String>,
}
impl EmailFilterCondition {
_impl!(in_mailboxes: Vec<Id>);
_impl!(in_mailbox_other_than: Vec<Id>);
_impl!(before: UtcDate);
_impl!(after: UtcDate);
_impl!(min_size: Option<u64>);
_impl!(max_size: Option<u64>);
_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<bool>);
_impl!(text: String);
_impl!(from: String);
_impl!(to: String);
_impl!(cc: String);
_impl!(bcc: String);
_impl!(subject: String);
_impl!(body: String);
_impl!(header: Vec<String>);
}
impl FilterTrait<EmailObject> for EmailFilterCondition {}
// The following convenience properties are also specified for the Email

+ 88
- 46
melib/src/backends/jmap/protocol.rs View File

@ -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<OBJ: Object>: Serialize {
const NAME: &'static str;
}
@ -68,19 +77,25 @@ pub struct Request {
/* Why is this Value instead of Box<dyn Method<_>>? The Method trait cannot be made into a
* Trait object because its serialize() will be generic. */
method_calls: Vec<Value>,
#[serde(skip_serializing)]
request_no: Arc<Mutex<usize>>,
}
impl Request {
pub fn new() -> Self {
pub fn new(request_no: Arc<Mutex<usize>>) -> Self {
Request {
using: USING,
method_calls: Vec::new(),
request_no,
}
}
pub fn add_call<M: Method<O>, O: Object>(&mut self, call: M) {
pub fn add_call<M: Method<O>, 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<FnvHashMap<FolderHash, JmapFolder>> {
pub fn get_mailboxes(conn: &JmapConnection) -> Result<FnvHashMap<FolderHash, JmapFolder>> {
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<Vec<String>> {
pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result<Vec<String>> {
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<Vec<Envelope>> {
pub fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
let seq = get_request_no!(conn.request_no);
let email_call: EmailGetCall =
EmailGetCall::new(GetCall::new().ids(Some(ids.iter().cloned().collect::<Vec<String>>())));
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::<Value>(&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
"bodyProperties": [ "partId", "blobId", "size", "type" ],
"fetchHTMLBodyValues": true,
"maxBodyValueBytes": 256
}, format!("#m{}", conn.request_no + 1).as_str()]],
}, format!("m{}", seq).as_str()]],
}))
.send();
conn.request_no += 1;
let v: JsonResponse = serde_json::from_str(&std::dbg!(res.unwrap().text().unwrap()))?;
std::dbg!(&v);
Ok(vec![])
}
*/

+ 97
- 8
melib/src/backends/jmap/rfc8620.rs View File

@ -19,14 +19,18 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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<OBJ: Object, CALL: Method<OBJ>>
pub struct JmapSession {
capabilities: HashMap<String, CapabilitiesObject>,
accounts: HashMap<Id, Account>,
primary_accounts: Vec<Id>,
username: String,
api_url: String,
download_url: String,
upload_url: String,
event_source_url: String,
state: String,
#[serde(flatten)]
extra_properties: HashMap<String, Value>,
}
#[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<String>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Account {
name: String,
is_personal: bool,
is_read_only: bool,
account_capabilities: HashMap<String, Value>,
#[serde(flatten)]
extra_properties: HashMap<String, Value>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetCall<OBJ: Object>
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<Vec<String>>,
pub ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<Vec<String>>,
_ph: PhantomData<*const CALL>,
__ph: PhantomData<*const OBJ>,
pub properties: Option<Vec<String>>,
_ph: PhantomData<*const OBJ>,
}
impl<OBJ: Object> GetCall<OBJ>
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<Vec<String>>);
_impl!(properties: Option<Vec<String>>);
}
// 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<F: FilterTrait<OBJ>, OBJ: Object> QueryCall<F, OBJ>
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<Filter<F, OBJ>>);
_impl!(sort: Option<Comparator<OBJ>>);
_impl!(position: u64);
_impl!(anchor: Option<String>);
_impl!(anchor_offset: u64);
_impl!(limit: Option<u64>);
_impl!(calculate_total: bool);
}
pub fn bool_false() -> bool {
false
}
fn bool_true() -> bool {
pub fn bool_true() -> bool {
true
}

+ 17
- 0
melib/src/backends/jmap/rfc8620/comparator.rs View File

@ -35,6 +35,23 @@ pub struct Comparator {
_ph: PhantomData<*const OBJ>,
}
impl<OBJ: Object> Comparator<OBJ> {
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<String>);
_impl!(additional_properties: Vec<String>);
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum FilterOperator {

Loading…
Cancel
Save