parent
52bcecfd4a
commit
c88eac1cc5
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue