ui: Add search for IMAP
Add basic search utilising the default SEARCH capability.jmap
parent
27edd96493
commit
99697a8fd5
|
@ -524,4 +524,29 @@ impl ImapType {
|
||||||
.map(|c| String::from_utf8_lossy(c).into())
|
.map(|c| String::from_utf8_lossy(c).into())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn search(&self, query: String) -> Result<crate::structs::StackVec<EnvelopeHash>> {
|
||||||
|
let mut response = String::with_capacity(8 * 1024);
|
||||||
|
let mut conn = self.connection.lock()?;
|
||||||
|
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query).as_bytes())?;
|
||||||
|
conn.read_response(&mut response)?;
|
||||||
|
|
||||||
|
let mut lines = response.lines();
|
||||||
|
for l in lines.by_ref() {
|
||||||
|
if l.starts_with("* SEARCH") {
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
let uid_index = self.uid_index.lock().unwrap();
|
||||||
|
return Ok(crate::structs::StackVec::from_iter(
|
||||||
|
l["* SEARCH".len()..]
|
||||||
|
.trim()
|
||||||
|
.split_whitespace()
|
||||||
|
.map(usize::from_str)
|
||||||
|
.filter_map(std::result::Result::ok)
|
||||||
|
.filter_map(|uid| uid_index.get(&uid))
|
||||||
|
.map(|env_hash_ref| *env_hash_ref),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(MeliError::new(response))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
185
ui/src/cache.rs
185
ui/src/cache.rs
|
@ -19,7 +19,7 @@
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use melib::email::{Flag, UnixTimestamp};
|
use melib::email::UnixTimestamp;
|
||||||
use melib::parsec::*;
|
use melib::parsec::*;
|
||||||
use melib::{
|
use melib::{
|
||||||
backends::{FolderHash, MailBackend},
|
backends::{FolderHash, MailBackend},
|
||||||
|
@ -51,15 +51,14 @@ pub enum Query {
|
||||||
Subject(String),
|
Subject(String),
|
||||||
AllText(String),
|
AllText(String),
|
||||||
/* * * * */
|
/* * * * */
|
||||||
Flag(Flag),
|
Flags(Vec<String>),
|
||||||
And(Box<Query>, Box<Query>),
|
And(Box<Query>, Box<Query>),
|
||||||
Or(Box<Query>, Box<Query>),
|
Or(Box<Query>, Box<Query>),
|
||||||
Not(Box<Query>),
|
Not(Box<Query>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod query_parser {
|
pub mod query_parser {
|
||||||
use super::Query::{self, *};
|
use super::*;
|
||||||
use melib::parsec::*;
|
|
||||||
|
|
||||||
fn subject<'a>() -> impl Parser<'a, Query> {
|
fn subject<'a>() -> impl Parser<'a, Query> {
|
||||||
prefix(
|
prefix(
|
||||||
|
@ -77,6 +76,30 @@ pub mod query_parser {
|
||||||
.map(|term| Query::From(term))
|
.map(|term| Query::From(term))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to<'a>() -> impl Parser<'a, Query> {
|
||||||
|
prefix(
|
||||||
|
whitespace_wrap(match_literal("to:")),
|
||||||
|
whitespace_wrap(literal()),
|
||||||
|
)
|
||||||
|
.map(|term| Query::To(term))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cc<'a>() -> impl Parser<'a, Query> {
|
||||||
|
prefix(
|
||||||
|
whitespace_wrap(match_literal("cc:")),
|
||||||
|
whitespace_wrap(literal()),
|
||||||
|
)
|
||||||
|
.map(|term| Query::Cc(term))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bcc<'a>() -> impl Parser<'a, Query> {
|
||||||
|
prefix(
|
||||||
|
whitespace_wrap(match_literal("bcc:")),
|
||||||
|
whitespace_wrap(literal()),
|
||||||
|
)
|
||||||
|
.map(|term| Query::Bcc(term))
|
||||||
|
}
|
||||||
|
|
||||||
fn or<'a>() -> impl Parser<'a, Query> {
|
fn or<'a>() -> impl Parser<'a, Query> {
|
||||||
move |input| {
|
move |input| {
|
||||||
whitespace_wrap(match_literal_anycase("or"))
|
whitespace_wrap(match_literal_anycase("or"))
|
||||||
|
@ -119,6 +142,35 @@ pub mod query_parser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flags<'a>() -> impl Parser<'a, Query> {
|
||||||
|
move |input| {
|
||||||
|
whitespace_wrap(match_literal_anycase("flags:"))
|
||||||
|
.parse(input)
|
||||||
|
.and_then(|(rest, _)| {
|
||||||
|
map(one_or_more(pred(any_char, |c| *c != ' ')), |chars| {
|
||||||
|
chars.into_iter().collect::<String>()
|
||||||
|
})
|
||||||
|
.parse(rest)
|
||||||
|
})
|
||||||
|
.and_then(|(rest, flags_list)| {
|
||||||
|
if let Ok(r) = flags_list
|
||||||
|
.split(",")
|
||||||
|
.map(|t| {
|
||||||
|
either(quoted_string(), string())
|
||||||
|
.parse_complete(t)
|
||||||
|
.map(|(_, r)| r)
|
||||||
|
})
|
||||||
|
.collect::<std::result::Result<Vec<String>, &str>>()
|
||||||
|
.map(|v| Flags(v))
|
||||||
|
{
|
||||||
|
Ok((rest, r))
|
||||||
|
} else {
|
||||||
|
Err(rest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parser from `String` to `Query`.
|
/// Parser from `String` to `Query`.
|
||||||
///
|
///
|
||||||
/// # Invocation
|
/// # Invocation
|
||||||
|
@ -133,13 +185,16 @@ pub mod query_parser {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn query<'a>() -> impl Parser<'a, Query> {
|
pub fn query<'a>() -> impl Parser<'a, Query> {
|
||||||
move |input| {
|
move |input| {
|
||||||
let (rest, query_a): (&'a str, Query) = if let Ok(q) = parentheses_query().parse(input)
|
let (rest, query_a): (&'a str, Query) = if let Ok(q) = parentheses_query()
|
||||||
|
.parse(input)
|
||||||
|
.or(from().parse(input))
|
||||||
|
.or(to().parse(input))
|
||||||
|
.or(cc().parse(input))
|
||||||
|
.or(bcc().parse(input))
|
||||||
|
.or(subject().parse(input))
|
||||||
|
.or(flags().parse(input))
|
||||||
{
|
{
|
||||||
Ok(q)
|
Ok(q)
|
||||||
} else if let Ok(q) = subject().parse(input) {
|
|
||||||
Ok(q)
|
|
||||||
} else if let Ok(q) = from().parse(input) {
|
|
||||||
Ok(q)
|
|
||||||
} else if let Ok((rest, query_a)) = not().parse(input) {
|
} else if let Ok((rest, query_a)) = not().parse(input) {
|
||||||
Ok((rest, Not(Box::new(query_a))))
|
Ok((rest, Not(Box::new(query_a))))
|
||||||
} else if let Ok((rest, query_a)) = {
|
} else if let Ok((rest, query_a)) = {
|
||||||
|
@ -250,5 +305,117 @@ pub mod query_parser {
|
||||||
"(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))"
|
"(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))),
|
||||||
|
query().parse_complete("flags:test,testtest")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_to_imap(q: &Query) -> String {
|
||||||
|
fn rec(q: &Query, s: &mut String) {
|
||||||
|
use crate::sqlite3::escape_double_quote;
|
||||||
|
match q {
|
||||||
|
Subject(t) => {
|
||||||
|
s.push_str(" SUBJECT \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
From(t) => {
|
||||||
|
s.push_str(" FROM \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
To(t) => {
|
||||||
|
s.push_str(" TO \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
Cc(t) => {
|
||||||
|
s.push_str(" CC \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
Bcc(t) => {
|
||||||
|
s.push_str(" BCC \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
AllText(t) => {
|
||||||
|
s.push_str(" TEXT \"");
|
||||||
|
s.extend(escape_double_quote(t).chars());
|
||||||
|
s.push_str("\"");
|
||||||
|
}
|
||||||
|
Flags(v) => {
|
||||||
|
for f in v {
|
||||||
|
match f.as_str() {
|
||||||
|
"draft" => {
|
||||||
|
s.push_str(" DRAFT ");
|
||||||
|
}
|
||||||
|
"deleted" => {
|
||||||
|
s.push_str(" DELETED ");
|
||||||
|
}
|
||||||
|
"flagged" => {
|
||||||
|
s.push_str(" FLAGGED ");
|
||||||
|
}
|
||||||
|
"recent" => {
|
||||||
|
s.push_str(" RECENT ");
|
||||||
|
}
|
||||||
|
"seen" | "read" => {
|
||||||
|
s.push_str(" SEEN ");
|
||||||
|
}
|
||||||
|
"unseen" | "unread" => {
|
||||||
|
s.push_str(" UNSEEN ");
|
||||||
|
}
|
||||||
|
"answered" => {
|
||||||
|
s.push_str(" ANSWERED ");
|
||||||
|
}
|
||||||
|
"unanswered" => {
|
||||||
|
s.push_str(" UNANSWERED ");
|
||||||
|
}
|
||||||
|
keyword => {
|
||||||
|
s.push_str(" ");
|
||||||
|
s.extend(keyword.chars());
|
||||||
|
s.push_str(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
And(q1, q2) => {
|
||||||
|
rec(q1, s);
|
||||||
|
s.push_str(" ");
|
||||||
|
rec(q2, s);
|
||||||
|
}
|
||||||
|
Or(q1, q2) => {
|
||||||
|
s.push_str(" OR ");
|
||||||
|
rec(q1, s);
|
||||||
|
s.push_str(" ");
|
||||||
|
rec(q2, s);
|
||||||
|
}
|
||||||
|
Not(q) => {
|
||||||
|
s.push_str(" NOT ");
|
||||||
|
rec(q, s);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut ret = String::new();
|
||||||
|
rec(q, &mut ret);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn imap_search(
|
||||||
|
term: &str,
|
||||||
|
(sort_field, sort_order): (SortField, SortOrder),
|
||||||
|
backend: &Arc<RwLock<Box<dyn MailBackend>>>,
|
||||||
|
) -> Result<StackVec<EnvelopeHash>> {
|
||||||
|
let query = query().parse(term)?.1;
|
||||||
|
let backend_lck = backend.read().unwrap();
|
||||||
|
|
||||||
|
let b = (*backend_lck).as_any();
|
||||||
|
if let Some(imap_backend) = b.downcast_ref::<melib::backends::ImapType>() {
|
||||||
|
imap_backend.search(query_to_imap(&query))
|
||||||
|
} else {
|
||||||
|
panic!("Could not downcast ImapType backend. BUG");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -820,7 +820,7 @@ impl Account {
|
||||||
folder_hash: FolderHash,
|
folder_hash: FolderHash,
|
||||||
) -> Result<StackVec<EnvelopeHash>> {
|
) -> Result<StackVec<EnvelopeHash>> {
|
||||||
if self.settings.account().format() == "imap" {
|
if self.settings.account().format() == "imap" {
|
||||||
return Err(MeliError::new("No search support for IMAP yet."));
|
return crate::cache::imap_search(search_term, sort, &self.backend);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sqlite3")]
|
#[cfg(feature = "sqlite3")]
|
||||||
|
|
|
@ -35,7 +35,7 @@ use std::convert::TryInto;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn escape_double_quote(w: &str) -> Cow<str> {
|
pub fn escape_double_quote(w: &str) -> Cow<str> {
|
||||||
if w.contains('"') {
|
if w.contains('"') {
|
||||||
Cow::from(w.replace('"', "\"\""))
|
Cow::from(w.replace('"', "\"\""))
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue