Browse Source

JMAP WIP

async
Manos Pitsidianakis 2 years ago
parent
commit
a43f6919cc
Signed by untrusted user: epilys GPG Key ID: 73627C2F690DF710
  1. 842
      Cargo.lock
  2. 5
      melib/Cargo.toml
  3. 14
      melib/src/backends.rs
  4. 295
      melib/src/backends/jmap.rs
  5. 78
      melib/src/backends/jmap/folder.rs
  6. 261
      melib/src/backends/jmap/protocol.rs
  7. 16
      melib/src/error.rs

842
Cargo.lock
File diff suppressed because it is too large
View File

5
melib/Cargo.toml

@ -26,9 +26,11 @@ bincode = "1.2.0"
uuid = { version = "0.7.4", features = ["serde", "v4"] }
text_processing = { path = "../text_processing", version = "*", optional= true }
libc = {version = "0.2.59", features = ["extra_traits",]}
reqwest = { version ="0.10.0-alpha.2", optional=true, features = ["json", "blocking" ]}
serde_json = { version = "1.0", optional = true }
[features]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard"]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "jmap_backend", "vcard"]
debug-tracing = []
unicode_algorithms = ["text_processing"]
@ -36,4 +38,5 @@ imap_backend = ["native-tls"]
maildir_backend = ["notify", "notify-rust", "memmap"]
mbox_backend = ["notify", "notify-rust", "memmap"]
notmuch_backend = []
jmap_backend = ["reqwest", "serde_json" ]
vcard = []

14
melib/src/backends.rs

@ -38,6 +38,10 @@ pub mod mbox;
pub mod notmuch;
#[cfg(feature = "notmuch_backend")]
pub use self::notmuch::NotmuchDb;
#[cfg(feature = "jmap_backend")]
pub mod jmap;
#[cfg(feature = "jmap_backend")]
pub use self::jmap::JmapType;
#[cfg(feature = "imap_backend")]
pub use self::imap::ImapType;
@ -129,6 +133,16 @@ impl Backends {
},
);
}
#[cfg(feature = "jmap_backend")]
{
b.register(
"jmap".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i| JmapType::new(f, i))),
validate_conf_fn: Box::new(JmapType::validate_config),
},
);
}
b
}

295
melib/src/backends/jmap.rs

@ -0,0 +1,295 @@
/*
* 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 crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
use crate::backends::BackendOp;
use crate::backends::FolderHash;
use crate::backends::RefreshEvent;
use crate::backends::RefreshEventKind::{self, *};
use crate::backends::{BackendFolder, Folder, FolderOperation, MailBackend, RefreshEventConsumer};
use crate::conf::AccountSettings;
use crate::email::*;
use crate::error::{MeliError, Result};
use fnv::{FnvHashMap, FnvHashSet};
use reqwest::blocking::Client;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
pub mod protocol;
use protocol::*;
pub mod folder;
use folder::*;
#[derive(Debug, Default)]
pub struct EnvelopeCache {
bytes: Option<String>,
headers: Option<String>,
body: Option<String>,
flags: Option<Flag>,
}
#[derive(Debug, Clone)]
pub struct JmapServerConf {
pub server_hostname: String,
pub server_username: String,
pub server_password: String,
pub server_port: u16,
pub danger_accept_invalid_certs: bool,
}
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {
$s.extra.get($var).ok_or_else(|| {
MeliError::new(format!(
"Configuration error ({}): JMAP connection requires the field `{}` set",
$s.name.as_str(),
$var
))
})
};
($s:ident[$var:literal], $default:expr) => {
$s.extra
.get($var)
.map(|v| {
<_>::from_str(v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
};
}
impl JmapServerConf {
pub fn new(s: &AccountSettings) -> Result<Self> {
Ok(JmapServerConf {
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
server_username: get_conf_val!(s["server_username"])?.to_string(),
server_password: get_conf_val!(s["server_password"])?.to_string(),
server_port: get_conf_val!(s["server_port"], 443)?,
danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?,
})
}
}
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
impl std::fmt::Debug for IsSubscribedFn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IsSubscribedFn Box")
}
}
impl std::ops::Deref for IsSubscribedFn {
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
&self.0
}
}
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",
$s.name.as_str(),
$var
))
})
};
($s:ident[$var:literal], $default:expr) => {
$s.extra
.get($var)
.map(|v| {
<_>::from_str(v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
};
}
#[derive(Debug)]
pub struct JmapType {
account_name: String,
online: Arc<Mutex<bool>>,
is_subscribed: Arc<IsSubscribedFn>,
server_conf: JmapServerConf,
connection: Arc<Mutex<JmapConnection>>,
folders: Arc<RwLock<FnvHashMap<FolderHash, JmapFolder>>>,
}
impl MailBackend for JmapType {
fn is_online(&self) -> bool {
*self.online.lock().unwrap()
}
fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
let mut w = AsyncBuilder::new();
let folders = self.folders.clone();
let connection = self.connection.clone();
let folder_hash = folder.hash();
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())
}),
))
.unwrap();
tx.send(AsyncStatus::Finished).unwrap();
};
Box::new(closure)
};
w.build(handle)
}
fn watch(
&self,
sender: RefreshEventConsumer,
work_context: WorkContext,
) -> Result<std::thread::ThreadId> {
Err(MeliError::from("sadfsa"))
}
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 ret = Ok(folders
.iter()
.map(|(&h, f)| (h, BackendFolder::clone(f) as Folder))
.collect());
*self.folders.write().unwrap() = folders;
ret
} else {
Ok(self
.folders
.read()
.unwrap()
.iter()
.map(|(&h, f)| (h, BackendFolder::clone(f) as Folder))
.collect())
}
}
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp> {
unimplemented!()
}
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
Ok(())
}
fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
Ok(())
}
fn as_any(&self) -> &dyn::std::any::Any {
self
}
}
impl JmapType {
pub fn new(
s: &AccountSettings,
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
) -> Result<Box<dyn MailBackend>> {
let online = Arc::new(Mutex::new(false));
let server_conf = JmapServerConf::new(s)?;
Ok(Box::new(JmapType {
connection: Arc::new(Mutex::new(JmapConnection::new(
&server_conf,
online.clone(),
)?)),
folders: Arc::new(RwLock::new(FnvHashMap::default())),
account_name: s.name.clone(),
online,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
server_conf,
}))
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"])?;
get_conf_val!(s["server_password"])?;
get_conf_val!(s["server_port"], 443)?;
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
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,
})
}
}

78
melib/src/backends/jmap/folder.rs

@ -0,0 +1,78 @@
/*
* 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::*;
use crate::backends::{FolderPermissions, SpecialUsageMailbox};
#[derive(Debug, Clone)]
pub struct JmapFolder {
pub name: String,
pub path: String,
pub hash: FolderHash,
pub v: Vec<FolderHash>,
pub id: String,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub parent_id: Option<String>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: u64,
pub total_threads: u64,
pub unread_emails: u64,
pub unread_threads: u64,
pub usage: SpecialUsageMailbox,
}
impl BackendFolder for JmapFolder {
fn hash(&self) -> FolderHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.path
}
fn change_name(&mut self, _s: &str) {}
fn clone(&self) -> Folder {
Box::new(std::clone::Clone::clone(self))
}
fn children(&self) -> &[FolderHash] {
&self.v
}
fn parent(&self) -> Option<FolderHash> {
None
}
fn permissions(&self) -> FolderPermissions {
FolderPermissions::default()
}
fn special_usage(&self) -> SpecialUsageMailbox {
self.usage
}
}

261
melib/src/backends/jmap/protocol.rs

@ -0,0 +1,261 @@
/*
* 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::folder::JmapFolder;
use super::*;
use serde_json::{json, Value};
macro_rules! get_path_hash {
($path:expr) => {{
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
static USING: &'static [&'static str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
pub fn get_mailboxes(conn: &mut JmapConnection) -> Result<FnvHashMap<FolderHash, JmapFolder>> {
let res = conn
.client
.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()]],
}))
.send();
conn.request_no += 1;
let mut v: JsonResponse =
serde_json::from_str(&std::dbg!(res.unwrap().text().unwrap())).unwrap();
*conn.online_status.lock().unwrap() = true;
std::dbg!(&v);
assert_eq!("Mailbox/get", v.method_responses[0].0);
Ok(
if let Response::MailboxGet { list, .. } = v.method_responses.remove(0).1 {
list.into_iter().map(|r| {
if let MailboxResponse {
id,
is_subscribed,
my_rights,
name,
parent_id,
role,
sort_order,
total_emails,
total_threads,
unread_emails,
unread_threads,
} = r
{
let hash = get_path_hash!(&name);
(
hash,
JmapFolder {
name: name.clone(),
hash,
path: name,
v: Vec::new(),
id,
is_subscribed,
my_rights,
parent_id,
role,
usage: Default::default(),
sort_order,
total_emails,
total_threads,
unread_emails,
unread_threads,
},
)
} else {
panic!()
}
})
} else {
panic!()
}
.collect(),
)
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct JsonResponse {
method_responses: Vec<MethodResponse>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MethodResponse(String, Response, String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum Response {
#[serde(rename_all = "camelCase")]
MailboxGet {
account_id: String,
list: Vec<MailboxResponse>,
not_found: Vec<String>,
state: String,
},
#[serde(rename_all = "camelCase")]
EmailQuery {
account_id: String,
can_calculate_changes: bool,
collapse_threads: bool,
filter: Value,
ids: Vec<String>,
position: u64,
query_state: String,
sort: Option<String>,
total: usize,
},
Empty {},
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MailboxResponse {
id: String,
is_subscribed: bool,
my_rights: JmapRights,
name: String,
parent_id: Option<String>,
role: Option<String>,
sort_order: u64,
total_emails: u64,
total_threads: u64,
unread_emails: u64,
unread_threads: u64,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JmapRights {
may_add_items: bool,
may_create_child: bool,
may_delete: bool,
may_read_items: bool,
may_remove_items: bool,
may_rename: bool,
may_set_keywords: bool,
may_set_seen: bool,
may_submit: bool,
}
// [
// [ "getMessageList", {
// filter: {
// inMailboxes: [ "mailbox1" ]
// },
// sort: [ "date desc", "id desc" ]
// collapseThreads: true,
// position: 0,
// limit: 10,
// fetchThreads: true,
// fetchMessages: true,
// fetchMessageProperties: [
// "threadId",
// "mailboxId",
// "isUnread",
// "isFlagged",
// "isAnswered",
// "isDraft",
// "hasAttachment",
// "from",
// "to",
// "subject",
// "date",
// "preview"
// ],
// fetchSearchSnippets: false
// }, "call1"]
// ]
pub fn get_message_list(conn: &mut JmapConnection, folder: &JmapFolder) -> Result<Vec<String>> {
let res = conn
.client
.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()]],
}))
.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;
if let Response::EmailQuery { ids, .. } = result {
Ok(ids)
} else {
Err(MeliError::new(format!("response was {:#?}", &result)))
}
}
pub fn get_message(conn: &mut JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
let res = conn
.client
.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/get", {
"ids": ids,
"properties": [ "threadId", "mailboxIds", "from", "subject",
"receivedAt",
"htmlBody", "bodyValues" ],
"bodyProperties": [ "partId", "blobId", "size", "type" ],
"fetchHTMLBodyValues": true,
"maxBodyValueBytes": 256
}, format!("#m{}", conn.request_no + 1).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![])
}

16
melib/src/error.rs

@ -151,6 +151,22 @@ impl From<std::num::ParseIntError> for MeliError {
}
}
#[cfg(feature = "jmap_backend")]
impl From<reqwest::Error> for MeliError {
#[inline]
fn from(kind: reqwest::Error) -> MeliError {
MeliError::new(format!("{}", kind))
}
}
#[cfg(feature = "jmap_backend")]
impl From<serde_json::error::Error> for MeliError {
#[inline]
fn from(kind: serde_json::error::Error) -> MeliError {
MeliError::new(format!("{}", kind))
}
}
impl From<&str> for MeliError {
#[inline]
fn from(kind: &str) -> MeliError {

Loading…
Cancel
Save