melib/jmap: implement search

Closes #59
memfd
Manos Pitsidianakis 2020-08-06 19:45:08 +03:00
parent 52bcecfd4a
commit c88eac1cc5
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
7 changed files with 343 additions and 24 deletions

View File

@ -26,6 +26,7 @@ use crate::email::*;
use crate::error::{MeliError, Result};
use reqwest::blocking::Client;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
@ -282,6 +283,66 @@ impl MailBackend for JmapType {
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.tag_index.clone())
}
fn search(
&self,
q: crate::search::Query,
mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
let conn = self.connection.clone();
let filter = if let Some(mailbox_hash) = mailbox_hash {
let mailbox_id = self.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let mut f = Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.into(),
);
f &= Filter::<EmailFilterCondition, EmailObject>::from(q);
f
} else {
Filter::<EmailFilterCondition, EmailObject>::from(q)
};
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.filter(Some(filter))
.position(0),
)
.collapse_threads(false);
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let res = conn
.client
.lock()
.unwrap()
.post(&conn.session.api_url)
.basic_auth(
&conn.server_conf.server_username,
Some(&conn.server_conf.server_password),
)
.json(&req)
.send();
let res_text = res?.text()?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().unwrap() = (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| {
use std::hash::Hasher;
let mut h = std::collections::hash_map::DefaultHasher::new();
h.write(id.as_bytes());
h.finish()
})
.collect();
Ok(Box::pin(async move { Ok(ret) }))
}
}
impl JmapType {

View File

@ -396,7 +396,7 @@ pub struct EmailQueryResponse {
#[serde(rename_all = "camelCase")]
pub struct EmailQuery {
#[serde(flatten)]
pub query_call: Query<EmailFilterCondition, EmailObject>,
pub query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
//pub filter: EmailFilterCondition, /* "inMailboxes": [ mailbox.id ] },*/
pub collapse_threads: bool,
}
@ -412,7 +412,7 @@ impl EmailQuery {
_ph: PhantomData,
};
pub fn new(query_call: Query<EmailFilterCondition, EmailObject>) -> Self {
pub fn new(query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>) -> Self {
EmailQuery {
query_call,
collapse_threads: false,
@ -540,6 +540,15 @@ impl EmailFilterCondition {
impl FilterTrait<EmailObject> for EmailFilterCondition {}
impl From<EmailFilterCondition> for FilterCondition<EmailFilterCondition, EmailObject> {
fn from(val: EmailFilterCondition) -> FilterCondition<EmailFilterCondition, EmailObject> {
FilterCondition {
cond: val,
_ph: PhantomData,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum MessageProperty {
@ -567,3 +576,148 @@ pub enum MessageProperty {
InReplyTo,
Sender,
}
impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
fn from(val: crate::search::Query) -> Self {
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
use crate::search::Query::*;
match q {
Subject(t) => {
*f = Filter::Condition(EmailFilterCondition::new().subject(t.clone()).into());
}
From(t) => {
*f = Filter::Condition(EmailFilterCondition::new().from(t.clone()).into());
}
To(t) => {
*f = Filter::Condition(EmailFilterCondition::new().to(t.clone()).into());
}
Cc(t) => {
*f = Filter::Condition(EmailFilterCondition::new().cc(t.clone()).into());
}
Bcc(t) => {
*f = Filter::Condition(EmailFilterCondition::new().bcc(t.clone()).into());
}
AllText(t) => {
*f = Filter::Condition(EmailFilterCondition::new().text(t.clone()).into());
}
Body(t) => {
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
}
Before(_) => {
//TODO, convert UNIX timestamp into UtcDate
}
After(_) => {
//TODO
}
Between(_, _) => {
//TODO
}
On(_) => {
//TODO
}
InReplyTo(_) => {
//TODO, look inside Headers
}
References(_) => {
//TODO
}
AllAddresses(_) => {
//TODO
}
Flags(v) => {
let mut accum = if let Some(first) = v.first() {
Filter::Condition(
EmailFilterCondition::new()
.has_keyword(first.to_string())
.into(),
)
} else {
Filter::Condition(EmailFilterCondition::new().into())
};
for f in v.iter().skip(1) {
accum &= Filter::Condition(
EmailFilterCondition::new()
.has_keyword(f.as_str().to_string())
.into(),
);
}
*f = accum;
}
HasAttachment => {
*f = Filter::Condition(
EmailFilterCondition::new()
.has_attachment(Some(true))
.into(),
);
}
And(q1, q2) => {
let mut rhs = Filter::Condition(EmailFilterCondition::new().into());
let mut lhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q1, &mut rhs);
rec(q2, &mut lhs);
rhs &= lhs;
*f = rhs;
}
Or(q1, q2) => {
let mut rhs = Filter::Condition(EmailFilterCondition::new().into());
let mut lhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q1, &mut rhs);
rec(q2, &mut lhs);
rhs |= lhs;
*f = rhs;
}
Not(q) => {
let mut qhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q, &mut qhs);
*f = !qhs;
}
}
}
rec(&val, &mut ret);
ret
}
}
#[test]
fn test_jmap_query() {
use std::sync::{Arc, Mutex};
let q: crate::search::Query = crate::search::Query::try_from(
"subject:wah or (from:Manos and (subject:foo or subject:bar))",
)
.unwrap();
let f: Filter<EmailFilterCondition, EmailObject> = Filter::from(q);
assert_eq!(
r#"{"operator":"OR","conditions":[{"subject":"wah"},{"operator":"AND","conditions":[{"from":"Manos"},{"operator":"OR","conditions":[{"subject":"foo"},{"subject":"bar"}]}]}]}"#,
serde_json::to_string(&f).unwrap().as_str()
);
let filter = {
let mailbox_id = "mailbox_id".to_string();
let mut r = Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.into(),
);
r &= f;
r
};
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id("account_id".to_string())
.filter(Some(filter))
.position(0),
)
.collapse_threads(false);
let request_no = Arc::new(Mutex::new(0));
let mut req = Request::new(request_no.clone());
req.add_call(&email_call);
assert_eq!(
r#"{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/query",{"accountId":"account_id","calculateTotal":false,"collapseThreads":false,"filter":{"conditions":[{"inMailbox":"mailbox_id"},{"conditions":[{"subject":"wah"},{"conditions":[{"from":"Manos"},{"conditions":[{"subject":"foo"},{"subject":"bar"}],"operator":"OR"}],"operator":"AND"}],"operator":"OR"}],"operator":"AND"},"position":0,"sort":null},"m0"]]}"#,
serde_json::to_string(&req).unwrap().as_str()
);
assert_eq!(*request_no.lock().unwrap(), 1);
}

View File

@ -175,9 +175,11 @@ pub fn get_message_list(conn: &JmapConnection, mailbox: &JmapMailbox) -> Result<
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.filter(Some(
EmailFilterCondition::new().in_mailbox(Some(mailbox.id.clone())),
))
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone()))
.into(),
)))
.position(0),
)
.collapse_threads(false);
@ -245,9 +247,11 @@ pub fn get(
let email_query_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().to_string())
.filter(Some(
EmailFilterCondition::new().in_mailbox(Some(mailbox.id.clone())),
))
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone()))
.into(),
)))
.position(0),
)
.collapse_threads(false);

View File

@ -51,11 +51,3 @@ impl<OBJ: Object> Comparator<OBJ> {
_impl!(collation: Option<String>);
_impl!(additional_properties: Vec<String>);
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum FilterOperator {
And,
Or,
Not,
}

View File

@ -21,7 +21,7 @@
use super::*;
pub trait FilterTrait<T> {}
pub trait FilterTrait<T>: Default {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
@ -33,18 +33,107 @@ pub enum Filter<F: FilterTrait<OBJ>, OBJ: Object> {
Condition(FilterCondition<F, OBJ>),
}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterTrait<OBJ> for Filter<F, OBJ> {}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterTrait<OBJ> for FilterCondition<F, OBJ> {}
#[derive(Serialize, Debug)]
pub struct FilterCondition<F: FilterTrait<OBJ>, OBJ: Object> {
#[serde(flatten)]
cond: F,
pub cond: F,
#[serde(skip)]
_ph: PhantomData<*const OBJ>,
pub _ph: PhantomData<*const OBJ>,
}
#[derive(Serialize, Debug)]
#[derive(Serialize, Debug, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum FilterOperator {
And,
Or,
Not,
}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterCondition<F, OBJ> {
pub fn new() -> Self {
FilterCondition {
cond: F::default(),
_ph: PhantomData,
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for FilterCondition<F, OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for Filter<F, OBJ> {
fn default() -> Self {
Filter::Condition(FilterCondition::default())
}
}
use std::ops::{BitAndAssign, BitOrAssign, Not};
impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
fn bitand_assign(&mut self, rhs: Self) {
match self {
Filter::Operator {
operator: FilterOperator::And,
ref mut conditions,
} => {
conditions.push(rhs);
}
Filter::Condition(_) | Filter::Operator { .. } => {
*self = Filter::Operator {
operator: FilterOperator::And,
conditions: vec![
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
rhs,
],
};
}
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> BitOrAssign for Filter<F, OBJ> {
fn bitor_assign(&mut self, rhs: Self) {
match self {
Filter::Operator {
operator: FilterOperator::Or,
ref mut conditions,
} => {
conditions.push(rhs);
}
Filter::Condition(_) | Filter::Operator { .. } => {
*self = Filter::Operator {
operator: FilterOperator::Or,
conditions: vec![
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
rhs,
],
};
}
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Not for Filter<F, OBJ> {
type Output = Self;
fn not(self) -> Self {
match self {
Filter::Operator {
operator,
conditions,
} if operator == FilterOperator::Not => Filter::Operator {
operator: FilterOperator::Or,
conditions,
},
Filter::Condition(_) | Filter::Operator { .. } => Filter::Operator {
operator: FilterOperator::Not,
conditions: vec![self],
},
}
}
}

View File

@ -22,6 +22,7 @@
use crate::parsec::*;
use crate::UnixTimestamp;
use std::borrow::Cow;
use std::convert::TryFrom;
pub use query_parser::query;
use Query::*;
@ -90,6 +91,16 @@ impl QueryTrait for crate::Envelope {
}
}
impl TryFrom<&str> for Query {
type Error = crate::error::MeliError;
fn try_from(t: &str) -> crate::error::Result<Query> {
query()
.parse_complete(t)
.map(|(_, q)| q)
.map_err(|err| err.into())
}
}
pub mod query_parser {
use super::*;
@ -361,6 +372,14 @@ pub mod query_parser {
query().parse_complete("flags:test,testtest"),
query().parse_complete("tags:test,testtest")
);
assert_eq!(
query().parse_complete("flags:seen"),
query().parse_complete("tags:seen")
);
assert_eq!(
Ok(("", Flags(vec!["f".to_string()]))),
query().parse_complete("tags:f")
);
}
}

View File

@ -46,6 +46,7 @@ pub use futures::stream::Stream;
use futures::stream::StreamExt;
use std::borrow::Cow;
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::fs;
use std::io;
use std::ops::{Index, IndexMut};
@ -388,7 +389,7 @@ impl Account {
}
};
if settings.account().format() == "imap" {
if ["imap", "jmap", "notmuch"].contains(&settings.account().format()) {
settings.conf.search_backend = crate::conf::SearchBackend::None;
}
@ -1673,9 +1674,7 @@ impl Account {
_sort: (SortField, SortOrder),
mailbox_hash: MailboxHash,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
use melib::parsec::Parser;
use melib::search::QueryTrait;
let query = melib::search::query().parse(search_term)?.1;
let query = melib::search::Query::try_from(search_term)?;
match self.settings.conf.search_backend {
#[cfg(feature = "sqlite3")]
crate::conf::SearchBackend::Sqlite3 => crate::sqlite3::search(&query, _sort),
@ -1686,6 +1685,7 @@ impl Account {
.unwrap()
.search(query, Some(mailbox_hash))
} else {
use melib::search::QueryTrait;
let mut ret = SmallVec::new();
let envelopes = self.collection.envelopes.read().unwrap();
for &env_hash in self.collection.get_mailbox(mailbox_hash).iter() {