Add `filter` option in mail list

Filter mail in mail list.

Example:
[listing]
filter = "not flags:seen" # show only unseen messages
memfd
Manos Pitsidianakis 2020-03-01 20:24:00 +02:00
parent 9d20fd5576
commit a3600c0cd2
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 155 additions and 23 deletions

31
meli.1
View File

@ -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.

View File

@ -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

View File

@ -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<Address>,
@ -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()),
);
}
}
/*

View File

@ -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<Query>),
}
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<str> {
Cow::from(w)
}
}
use serde::{de, Deserialize, Deserializer};
impl<'de> Deserialize<'de> for Query {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
let _q = query().parse(&s);
if let Some(q) = _q.ok() {
Ok(q.1)
} else {
Err(de::Error::custom("invalid query value"))
}
}
}

View File

@ -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();

View File

@ -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<Query>,
}