From a3600c0cd2449c45871bbca24305f22ef82e2e6f Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 1 Mar 2020 20:24:00 +0200 Subject: [PATCH] Add `filter` option in mail list Filter mail in mail list. Example: [listing] filter = "not flags:seen" # show only unseen messages --- meli.1 | 31 +++++++++++++ meli.conf.5 | 10 +++++ melib/src/email.rs | 61 ++++++++++++++++++-------- src/cache.rs | 56 ++++++++++++++++++++++- src/components/mail/listing/compact.rs | 14 ++++-- src/conf/listing.rs | 6 +++ 6 files changed, 155 insertions(+), 23 deletions(-) diff --git a/meli.1 b/meli.1 index ce0b21c2..3527b9c1 100644 --- a/meli.1 +++ b/meli.1 @@ -430,6 +430,37 @@ attachment according to its mailcap entry. .It Ic v (un)selects mail entries in mail listings .El +.Sh QUERY ABNF SYNTAX +.Bl -bullet +.It +.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query +.It +.Li not = \&"not\&" | \&"!\&" +.It +.Li quoted = ALPHA / SP *(ALPHA / DIGIT / SP) +.It +.Li term = ALPHA *(ALPHA / DIGIT) | DQUOTE quoted DQUOTE +.It +.Li tagname = term +.It +.Li flagval = \&"passed\&" | \&"replied\&" | \&"seen\&" | \&"read\&" | \&"junk\&" | \&"trash\&" | \&"trashed\&" | \&"draft\&" | \&"flagged\&" | tagname +.It +.Li flagterm = flagval | flagval \&",\&" flagterm +.It +.Li from = \&"from:\&" term +.It +.Li to = \&"to:\&" term +.It +.Li cc = \&"cc:\&" term +.It +.Li bcc = \&"bcc:\&" term +.It +.Li alladdresses = \&"alladdresses:\&" term +.It +.Li subject = \&"subject:\&" term +.It +.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag +.El .Sh EXIT STATUS .Nm exits with 0 on a successful run. diff --git a/meli.conf.5 b/meli.conf.5 index f1cc7bd6..542833da 100644 --- a/meli.conf.5 +++ b/meli.conf.5 @@ -607,6 +607,16 @@ this is usually what you want (optional) Show recent dates as `X {minutes,hours,days} ago`, up to 7 days. .\" default value .Pq Em true +.It Ic filter Ar Query +(optional) Show only envelopes matching this query (for query syntax see +.Xr meli 1 ) +.\" default value +.Pq Em None +.Pp +Example: +.Bd -literal +filter = "not flags:seen" # show only unseen messages +.Ed .El .Sh TAGS .Bl -tag -width 36n diff --git a/melib/src/email.rs b/melib/src/email.rs index 56ad9158..dc309db2 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -65,6 +65,20 @@ bitflags! { } } +impl PartialEq<&str> for Flag { + fn eq(&self, other: &&str) -> bool { + (other.eq_ignore_ascii_case("passed") && self.contains(Flag::PASSED)) + || (other.eq_ignore_ascii_case("replied") && self.contains(Flag::REPLIED)) + || (other.eq_ignore_ascii_case("seen") && self.contains(Flag::SEEN)) + || (other.eq_ignore_ascii_case("read") && self.contains(Flag::SEEN)) + || (other.eq_ignore_ascii_case("junk") && self.contains(Flag::TRASHED)) + || (other.eq_ignore_ascii_case("trash") && self.contains(Flag::TRASHED)) + || (other.eq_ignore_ascii_case("trashed") && self.contains(Flag::TRASHED)) + || (other.eq_ignore_ascii_case("draft") && self.contains(Flag::DRAFT)) + || (other.eq_ignore_ascii_case("flagged") && self.contains(Flag::FLAGGED)) + } +} + #[derive(Debug, Clone, Default)] pub struct EnvelopeWrapper { envelope: Envelope, @@ -113,7 +127,7 @@ pub type EnvelopeHash = u64; /// Access to the underlying email object in the account's backend (for example the file or the /// entry in an IMAP server) is given through `operation_token`. For more information see /// `BackendOp`. -#[derive(Clone, Default, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Envelope { date: String, from: Vec
, @@ -150,6 +164,12 @@ impl fmt::Debug for Envelope { } } +impl Default for Envelope { + fn default() -> Self { + Envelope::new(0) + } +} + impl Envelope { pub fn new(hash: EnvelopeHash) -> Self { Envelope { @@ -162,7 +182,17 @@ impl Envelope { message_id: MessageID::default(), in_reply_to: None, references: None, - other_headers: FnvHashMap::default(), + other_headers: [ + ("From".to_string(), String::new()), + ("To".to_string(), String::new()), + ("Subject".to_string(), String::new()), + ("Date".to_string(), String::new()), + ("Cc".to_string(), String::new()), + ("Bcc".to_string(), String::new()), + ] + .iter() + .cloned() + .collect(), timestamp: 0, @@ -218,9 +248,17 @@ impl Envelope { let mut in_reply_to = None; for (name, value) in headers { - if value.len() == 1 && value.is_empty() { - continue; - } + self.other_headers.insert( + String::from_utf8(name.to_vec()) + .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()), + parser::phrase(value, false) + .to_full_result() + .map(|value| { + String::from_utf8(value) + .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()) + }) + .unwrap_or_else(|_| String::from_utf8_lossy(value).into()), + ); if name.eq_ignore_ascii_case(b"to") { let parse_result = parser::rfc2822address_list(value); if parse_result.is_done() { @@ -298,19 +336,6 @@ impl Envelope { } _ => {} } - } else { - self.other_headers.insert( - String::from_utf8(name.to_vec()) - .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()), - parser::phrase(value, false) - .to_full_result() - .map(|value| { - String::from_utf8(value).unwrap_or_else(|err| { - String::from_utf8_lossy(&err.into_bytes()).into() - }) - }) - .unwrap_or_else(|_| String::from_utf8_lossy(value).into()), - ); } } /* diff --git a/src/cache.rs b/src/cache.rs index e4de72aa..078f2ef4 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -35,7 +35,7 @@ use std::sync::{Arc, RwLock}; pub use query_parser::query; use Query::*; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub enum Query { Before(UnixTimestamp), After(UnixTimestamp), @@ -61,6 +61,44 @@ pub enum Query { Not(Box), } +pub trait QueryTrait { + fn is_match(&self, query: &Query) -> bool; +} + +impl QueryTrait for melib::Envelope { + fn is_match(&self, query: &Query) -> bool { + use Query::*; + match query { + Before(timestamp) => self.date() < *timestamp, + After(timestamp) => self.date() > *timestamp, + Between(timestamp_a, timestamp_b) => { + self.date() > *timestamp_a && self.date() < *timestamp_b + } + On(timestamp) => { + self.date() > timestamp.saturating_sub(60 * 60 * 24) + && self.date() < *timestamp + 60 * 60 * 24 + } + From(s) => self.other_headers()["From"].contains(s), + To(s) => self.other_headers()["To"].contains(s), + Cc(s) => self.other_headers()["Cc"].contains(s), + Bcc(s) => self.other_headers()["Bcc"].contains(s), + AllAddresses(s) => { + self.is_match(&From(s.clone())) + || self.is_match(&To(s.clone())) + || self.is_match(&Cc(s.clone())) + || self.is_match(&Bcc(s.clone())) + } + Flags(v) => v.iter().any(|s| self.flags() == s.as_str()), + Subject(s) => self.other_headers()["Subject"].contains(s), + HasAttachment => self.has_attachments(), + 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, + } + } +} + pub mod query_parser { use super::*; @@ -451,3 +489,19 @@ pub fn escape_double_quote(w: &str) -> Cow { Cow::from(w) } } + +use serde::{de, Deserialize, Deserializer}; +impl<'de> Deserialize<'de> for Query { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + let _q = query().parse(&s); + if let Some(q) = _q.ok() { + Ok(q.1) + } else { + Err(de::Error::custom("invalid query value")) + } + } +} diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index 00132b3a..8ef30776 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -737,8 +737,7 @@ impl CompactListing { SmallVec::new(), ); - for (idx, thread) in items.enumerate() { - self.length += 1; + for thread in items { let thread_node = &threads.thread_nodes()[&threads.thread_ref(thread).root()]; let root_env_hash = thread_node.message().unwrap_or_else(|| { let mut iter_ptr = thread_node.children()[0]; @@ -761,6 +760,12 @@ impl CompactListing { let root_envelope: EnvelopeRef = context.accounts[self.cursor_pos.0] .collection .get_env(root_env_hash); + use crate::cache::{Query, QueryTrait}; + if let Some(filter_query) = context.settings.listing.filter.as_ref() { + if !root_envelope.is_match(filter_query) { + continue; + } + } let entry_strings = self.make_entry_string(&root_envelope, context, threads, thread); row_widths.1.push( @@ -796,11 +801,12 @@ impl CompactListing { min_width.4, entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(), ); /* subject */ - rows.push(((idx, (thread, root_env_hash)), entry_strings)); + rows.push(((self.length, (thread, root_env_hash)), entry_strings)); self.all_threads.insert(thread); - self.order.insert(thread, idx); + self.order.insert(thread, self.length); self.selection.insert(thread, false); + self.length += 1; } min_width.0 = self.length.saturating_sub(1).to_string().len(); diff --git a/src/conf/listing.rs b/src/conf/listing.rs index 7d382502..a984eebf 100644 --- a/src/conf/listing.rs +++ b/src/conf/listing.rs @@ -20,6 +20,7 @@ */ use super::default_vals::*; +use crate::cache::Query; /// Settings for mail listings #[derive(Debug, Deserialize, Clone, Default, Serialize)] @@ -38,4 +39,9 @@ pub struct ListingSettings { /// Default: true #[serde(default = "true_val")] pub recent_dates: bool, + + /// Show only envelopes that match this query + /// Default: None + #[serde(default = "none")] + pub filter: Option, }