Add `filter` option in mail list
Filter mail in mail list. Example: [listing] filter = "not flags:seen" # show only unseen messagesmemfd
parent
9d20fd5576
commit
a3600c0cd2
31
meli.1
31
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.
|
||||
|
|
10
meli.conf.5
10
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
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
|
56
src/cache.rs
56
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<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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue