From 6bf1756de844386ba312d15109ae29951896147b Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 4 Jun 2023 13:54:20 +0300 Subject: [PATCH] melib/search: implement more search criteria in Query type --- melib/src/backends/imap/search.rs | 96 +++++++++- melib/src/backends/mbox/write.rs | 7 +- melib/src/backends/notmuch.rs | 4 + melib/src/email/compose.rs | 7 +- melib/src/search.rs | 175 +++++++++++++++-- melib/src/utils/datetime.rs | 299 ++++++++++++++++-------------- melib/src/utils/parsec.rs | 22 ++- src/components/mail/view.rs | 8 +- src/sqlite3.rs | 43 +++-- 9 files changed, 471 insertions(+), 190 deletions(-) diff --git a/melib/src/backends/imap/search.rs b/melib/src/backends/imap/search.rs index 309a1498..bdb8c709 100644 --- a/melib/src/backends/imap/search.rs +++ b/melib/src/backends/imap/search.rs @@ -24,7 +24,7 @@ use std::collections::VecDeque; use crate::{ - datetime::{timestamp_to_string, IMAP_DATE}, + datetime::{formats::IMAP_DATE, timestamp_to_string}, search::*, }; @@ -41,8 +41,11 @@ impl private::Sealed for Query {} macro_rules! space_pad { ($s:ident) => {{ - if !$s.is_empty() { + if !$s.is_empty() && !$s.ends_with('(') && !$s.ends_with(' ') { $s.push(' '); + false + } else { + true } }}; } @@ -65,36 +68,43 @@ impl ToImapSearch for Query { s.push(lit); } Q(Subject(t)) => { + space_pad!(s); s.push_str("SUBJECT \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(From(t)) => { + space_pad!(s); s.push_str("FROM \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(To(t)) => { + space_pad!(s); s.push_str("TO \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(Cc(t)) => { + space_pad!(s); s.push_str("CC \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(Bcc(t)) => { + space_pad!(s); s.push_str("BCC \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(AllText(t)) => { + space_pad!(s); s.push_str("TEXT \""); s.extend(escape_double_quote(t).chars()); s.push('"'); } Q(Flags(v)) => { + space_pad!(s); for f in v { match f.as_str() { "draft" => { @@ -130,21 +140,26 @@ impl ToImapSearch for Query { } } Q(And(q1, q2)) => { - space_pad!(s); + let is_empty = space_pad!(s); + if !is_empty { + stack.push_front(Lit(')')); + } stack.push_front(Q(q2)); - stack.push_front(Lit(' ')); stack.push_front(Q(q1)); + if !is_empty { + stack.push_front(Lit('(')); + } } Q(Or(q1, q2)) => { space_pad!(s); - s.push_str("OR "); + s.push_str("OR"); stack.push_front(Q(q2)); - stack.push_front(Lit(' ')); stack.push_front(Q(q1)); } Q(Not(q)) => { space_pad!(s); - s.push_str("NOT "); + s.push_str("NOT ("); + stack.push_front(Lit(')')); stack.push_front(Q(q)); } Q(Before(t)) => { @@ -182,8 +197,23 @@ impl ToImapSearch for Query { s.extend(escape_double_quote(t).chars()); s.push('"'); } - Q(AllAddresses(_)) => { - // From OR To OR Cc OR Bcc + Q(AllAddresses(t)) => { + let is_empty = space_pad!(s); + if !is_empty { + s.push('('); + } + s.push_str("OR FROM \""); + s.extend(escape_double_quote(t).chars()); + s.push_str("\" (OR TO \""); + s.extend(escape_double_quote(t).chars()); + s.push_str("\" (OR CC \""); + s.extend(escape_double_quote(t).chars()); + s.push_str("\" BCC \""); + s.extend(escape_double_quote(t).chars()); + s.push_str(r#""))"#); + if !is_empty { + s.push(')'); + } } Q(Body(t)) => { space_pad!(s); @@ -192,10 +222,33 @@ impl ToImapSearch for Query { s.push('"'); } Q(HasAttachment) => { - // ??? + log::warn!("HasAttachment in IMAP is unimplemented."); + } + Q(Answered) => { + space_pad!(s); + s.push_str(r#"ANSWERED ""#); + } + Q(AnsweredBy { by }) => { + space_pad!(s); + s.push_str(r#"HEADER "From" ""#); + s.extend(escape_double_quote(by).chars()); + s.push('"'); + } + Q(Larger { than }) => { + space_pad!(s); + s.push_str("LARGER "); + s.push_str(&than.to_string()); + } + Q(Smaller { than }) => { + space_pad!(s); + s.push_str("SMALLER "); + s.push_str(&than.to_string()); } } } + while s.ends_with(' ') { + s.pop(); + } s } } @@ -231,5 +284,28 @@ mod tests { ×tamp_to_string(1685739600, Some(IMAP_DATE), true), "03-Jun-2023" ); + + let (_, q) = query() + .parse_complete("before:2023-06-04 from:user@example.org") + .unwrap(); + assert_eq!( + &q.to_imap_search(), + r#"BEFORE 04-Jun-2023 FROM "user@example.org""# + ); + let (_, q) = query() + .parse_complete(r#"subject:"wah ah ah" or (from:Manos and from:Sia)"#) + .unwrap(); + assert_eq!( + &q.to_imap_search(), + r#"OR SUBJECT "wah ah ah" (FROM "Manos" FROM "Sia")"# + ); + + let (_, q) = query() + .parse_complete(r#"subject:wo or (all-addresses:Manos)"#) + .unwrap(); + assert_eq!( + &q.to_imap_search(), + r#"OR SUBJECT "wo" (OR FROM "Manos" (OR TO "Manos" (OR CC "Manos" BCC "Manos")))"# + ); } } diff --git a/melib/src/backends/mbox/write.rs b/melib/src/backends/mbox/write.rs index b156055a..a9b41a2b 100644 --- a/melib/src/backends/mbox/write.rs +++ b/melib/src/backends/mbox/write.rs @@ -20,6 +20,7 @@ */ use super::*; +use crate::datetime; impl MboxFormat { pub fn append( @@ -49,9 +50,9 @@ impl MboxFormat { } writer.write_all(&b" "[..])?; writer.write_all( - crate::datetime::timestamp_to_string( - delivery_date.unwrap_or_else(crate::datetime::now), - Some(crate::datetime::ASCTIME_FMT), + datetime::timestamp_to_string( + delivery_date.unwrap_or_else(datetime::now), + Some(datetime::formats::ASCTIME_FMT), true, ) .trim() diff --git a/melib/src/backends/notmuch.rs b/melib/src/backends/notmuch.rs index e98c5a17..17a70477 100644 --- a/melib/src/backends/notmuch.rs +++ b/melib/src/backends/notmuch.rs @@ -1258,6 +1258,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query { q.query_to_string(ret); ret.push_str("))"); } + Answered => todo!(), + AnsweredBy { .. } => todo!(), + Larger { .. } => todo!(), + Smaller { .. } => todo!(), } } } diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index f8448c26..4e533752 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -33,6 +33,7 @@ use xdg_utils::query_mime_info; use super::*; use crate::{ + datetime, email::{ attachment_types::{Charset, ContentTransferEncoding, ContentType, MultipartType}, attachments::AttachmentBuilder, @@ -61,9 +62,9 @@ impl Default for Draft { let mut headers = HeaderMap::default(); headers.insert( HeaderName::DATE, - crate::datetime::timestamp_to_string( - crate::datetime::now(), - Some(crate::datetime::RFC822_DATE), + datetime::timestamp_to_string( + datetime::now(), + Some(datetime::formats::RFC822_DATE), true, ), ); diff --git a/melib/src/search.rs b/melib/src/search.rs index b23f5560..cedcca76 100644 --- a/melib/src/search.rs +++ b/melib/src/search.rs @@ -24,7 +24,10 @@ use std::{borrow::Cow, convert::TryFrom}; pub use query_parser::query; use Query::*; -use crate::{parsec::*, UnixTimestamp}; +use crate::{ + datetime::{formats, UnixTimestamp}, + parsec::*, +}; #[derive(Debug, PartialEq, Clone, Serialize)] pub enum Query { @@ -50,6 +53,18 @@ pub enum Query { And(Box, Box), Or(Box, Box), Not(Box), + /// By us. + Answered, + /// By an address/name. + AnsweredBy { + by: String, + }, + Larger { + than: usize, + }, + Smaller { + than: usize, + }, } pub trait QueryTrait { @@ -85,7 +100,38 @@ impl QueryTrait for crate::Envelope { And(q_a, q_b) => self.is_match(q_a) && self.is_match(q_b), Or(q_a, q_b) => self.is_match(q_a) || self.is_match(q_b), Not(q) => !self.is_match(q), - _ => false, + InReplyTo(_) => { + log::warn!("Filtering with InReplyTo is unimplemented."); + false + } + References(_) => { + log::warn!("Filtering with References is unimplemented."); + false + } + AllText(_) => { + log::warn!("Filtering with AllText is unimplemented."); + false + } + Body(_) => { + log::warn!("Filtering with Body is unimplemented."); + false + } + Answered => { + log::warn!("Filtering with Answered is unimplemented."); + false + } + AnsweredBy { .. } => { + log::warn!("Filtering with AnsweredBy is unimplemented."); + false + } + Larger { .. } => { + log::warn!("Filtering with Larger is unimplemented."); + false + } + Smaller { .. } => { + log::warn!("Filtering with Smaller is unimplemented."); + false + } } } } @@ -103,6 +149,87 @@ impl TryFrom<&str> for Query { pub mod query_parser { use super::*; + fn date<'a>() -> impl Parser<'a, UnixTimestamp> { + move |input| { + literal().parse(input).and_then(|(next_input, result)| { + if let Ok((_, t)) = + crate::datetime::parse_timestamp_from_string(result, formats::RFC3339_DATE) + { + Ok((next_input, t)) + } else { + Err(next_input) + } + }) + } + } + + fn before<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("before:")), + whitespace_wrap(date()), + ) + .map(Query::Before) + } + + fn after<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("after:")), + whitespace_wrap(date()), + ) + .map(Query::After) + } + + fn between<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("between:")), + pair( + suffix(whitespace_wrap(date()), whitespace_wrap(match_literal(","))), + whitespace_wrap(date()), + ), + ) + .map(|(t1, t2)| Query::Between(t1, t2)) + } + + fn on<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("on:")), + whitespace_wrap(date()), + ) + .map(Query::After) + } + + fn smaller<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("smaller:")), + whitespace_wrap(integer()), + ) + .map(|than| Query::Smaller { than }) + } + + fn larger<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("larger:")), + whitespace_wrap(integer()), + ) + .map(|than| Query::Larger { than }) + } + + fn answered_by<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("answered-by:")), + whitespace_wrap(literal()), + ) + .map(|by| Query::AnsweredBy { by }) + } + + fn answered<'a>() -> impl Parser<'a, Query> { + move |input| { + whitespace_wrap(match_literal_anycase("answered")) + .map(|()| Query::Answered) + .parse(input) + } + } + fn subject<'a>() -> impl Parser<'a, Query> { prefix( whitespace_wrap(match_literal("subject:")), @@ -143,6 +270,14 @@ pub mod query_parser { .map(Query::Bcc) } + fn all_addresses<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("all-addresses:")), + whitespace_wrap(literal()), + ) + .map(Query::AllAddresses) + } + fn or<'a>() -> impl Parser<'a, Query> { move |input| { whitespace_wrap(match_literal_anycase("or")) @@ -249,8 +384,17 @@ pub mod query_parser { .or_else(|_| to().parse(input)) .or_else(|_| cc().parse(input)) .or_else(|_| bcc().parse(input)) + .or_else(|_| all_addresses().parse(input)) .or_else(|_| subject().parse(input)) + .or_else(|_| before().parse(input)) + .or_else(|_| after().parse(input)) + .or_else(|_| on().parse(input)) + .or_else(|_| between().parse(input)) .or_else(|_| flags().parse(input)) + .or_else(|_| answered().parse(input)) + .or_else(|_| answered_by().parse(input)) + .or_else(|_| larger().parse(input)) + .or_else(|_| smaller().parse(input)) .or_else(|_| has_attachment().parse(input)) { Ok(q) @@ -282,7 +426,7 @@ pub mod query_parser { } else if let Ok((rest, query_b)) = or().parse(rest) { Ok((rest, Or(Box::new(query_a), Box::new(query_b)))) } else if let Ok((rest, query_b)) = query().parse(rest) { - Ok((rest, Or(Box::new(query_a), Box::new(query_b)))) + Ok((rest, And(Box::new(query_a), Box::new(query_b)))) } else { Ok((rest, query_a)) } @@ -321,8 +465,8 @@ mod tests { #[test] fn test_query_parsing() { assert_eq!( - Err("subject: test and"), - query().parse_complete("subject: test and") + Err("subject:test and"), + query().parse_complete("subject:test and") ); assert_eq!( Ok(( @@ -332,7 +476,7 @@ mod tests { Box::new(AllText("i".to_string())) ) )), - query().parse_complete("subject: test and i") + query().parse_complete("subject:test and i") ); assert_eq!( Ok(("", AllText("test".to_string()))), @@ -340,7 +484,17 @@ mod tests { ); assert_eq!( Ok(("", Subject("test".to_string()))), - query().parse_complete("subject: test") + query().parse_complete("subject:test") + ); + assert_eq!( + Ok(( + "", + And( + Box::new(From("Manos".to_string())), + Box::new(From("Sia".to_string())) + ) + )), + query().parse_complete("from:Manos and from:Sia") ); assert_eq!( Ok(( @@ -353,7 +507,7 @@ mod tests { )) ) )), - query().parse_complete("subject: \"wah ah ah\" or (from: Manos and from: Sia)") + query().parse_complete("subject:\"wah ah ah\" or (from:Manos and from:Sia)") ); assert_eq!( Ok(( @@ -369,8 +523,7 @@ mod tests { )) ) )), - query() - .parse_complete("subject: wah or (from: Manos and (subject:foo or subject: bar))") + query().parse_complete("subject:wah or (from:Manos and (subject:foo or subject:bar))") ); assert_eq!( Ok(( @@ -390,7 +543,7 @@ mod tests { ) )), query().parse_complete( - "(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!( diff --git a/melib/src/utils/datetime.rs b/melib/src/utils/datetime.rs index 04ceaf3a..8fc1d79f 100644 --- a/melib/src/utils/datetime.rs +++ b/melib/src/utils/datetime.rs @@ -47,17 +47,27 @@ use std::{ use crate::error::{Result, ResultIntoError}; pub type UnixTimestamp = u64; -pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0"; -pub const RFC3339_FMT: &str = "%Y-%m-%d\0"; -pub const RFC822_DATE: &str = "%a, %d %b %Y %H:%M:%S %z\0"; -pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0"; -pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0"; -pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0"; -//"Tue May 21 13:46:22 1991\n" -//"Wed Sep 9 00:27:54 2020\n" -pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0"; -/// Source: RFC3501 Section 9. Formal syntax, item `date-text` -pub const IMAP_DATE: &str = "%d-%b-%Y\0"; + +pub mod formats { + /// T