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
|
.It Ic v
|
||||||
(un)selects mail entries in mail listings
|
(un)selects mail entries in mail listings
|
||||||
.El
|
.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
|
.Sh EXIT STATUS
|
||||||
.Nm
|
.Nm
|
||||||
exits with 0 on a successful run.
|
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.
|
(optional) Show recent dates as `X {minutes,hours,days} ago`, up to 7 days.
|
||||||
.\" default value
|
.\" default value
|
||||||
.Pq Em true
|
.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
|
.El
|
||||||
.Sh TAGS
|
.Sh TAGS
|
||||||
.Bl -tag -width 36n
|
.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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct EnvelopeWrapper {
|
pub struct EnvelopeWrapper {
|
||||||
envelope: Envelope,
|
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
|
/// 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
|
/// entry in an IMAP server) is given through `operation_token`. For more information see
|
||||||
/// `BackendOp`.
|
/// `BackendOp`.
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Envelope {
|
pub struct Envelope {
|
||||||
date: String,
|
date: String,
|
||||||
from: Vec<Address>,
|
from: Vec<Address>,
|
||||||
|
@ -150,6 +164,12 @@ impl fmt::Debug for Envelope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Envelope {
|
||||||
|
fn default() -> Self {
|
||||||
|
Envelope::new(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Envelope {
|
impl Envelope {
|
||||||
pub fn new(hash: EnvelopeHash) -> Self {
|
pub fn new(hash: EnvelopeHash) -> Self {
|
||||||
Envelope {
|
Envelope {
|
||||||
|
@ -162,7 +182,17 @@ impl Envelope {
|
||||||
message_id: MessageID::default(),
|
message_id: MessageID::default(),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
references: 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,
|
timestamp: 0,
|
||||||
|
|
||||||
|
@ -218,9 +248,17 @@ impl Envelope {
|
||||||
let mut in_reply_to = None;
|
let mut in_reply_to = None;
|
||||||
|
|
||||||
for (name, value) in headers {
|
for (name, value) in headers {
|
||||||
if value.len() == 1 && value.is_empty() {
|
self.other_headers.insert(
|
||||||
continue;
|
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") {
|
if name.eq_ignore_ascii_case(b"to") {
|
||||||
let parse_result = parser::rfc2822address_list(value);
|
let parse_result = parser::rfc2822address_list(value);
|
||||||
if parse_result.is_done() {
|
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;
|
pub use query_parser::query;
|
||||||
use Query::*;
|
use Query::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||||
pub enum Query {
|
pub enum Query {
|
||||||
Before(UnixTimestamp),
|
Before(UnixTimestamp),
|
||||||
After(UnixTimestamp),
|
After(UnixTimestamp),
|
||||||
|
@ -61,6 +61,44 @@ pub enum Query {
|
||||||
Not(Box<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 {
|
pub mod query_parser {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -451,3 +489,19 @@ pub fn escape_double_quote(w: &str) -> Cow<str> {
|
||||||
Cow::from(w)
|
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(),
|
SmallVec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (idx, thread) in items.enumerate() {
|
for thread in items {
|
||||||
self.length += 1;
|
|
||||||
let thread_node = &threads.thread_nodes()[&threads.thread_ref(thread).root()];
|
let thread_node = &threads.thread_nodes()[&threads.thread_ref(thread).root()];
|
||||||
let root_env_hash = thread_node.message().unwrap_or_else(|| {
|
let root_env_hash = thread_node.message().unwrap_or_else(|| {
|
||||||
let mut iter_ptr = thread_node.children()[0];
|
let mut iter_ptr = thread_node.children()[0];
|
||||||
|
@ -761,6 +760,12 @@ impl CompactListing {
|
||||||
let root_envelope: EnvelopeRef = context.accounts[self.cursor_pos.0]
|
let root_envelope: EnvelopeRef = context.accounts[self.cursor_pos.0]
|
||||||
.collection
|
.collection
|
||||||
.get_env(root_env_hash);
|
.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);
|
let entry_strings = self.make_entry_string(&root_envelope, context, threads, thread);
|
||||||
row_widths.1.push(
|
row_widths.1.push(
|
||||||
|
@ -796,11 +801,12 @@ impl CompactListing {
|
||||||
min_width.4,
|
min_width.4,
|
||||||
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
||||||
); /* subject */
|
); /* 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.all_threads.insert(thread);
|
||||||
|
|
||||||
self.order.insert(thread, idx);
|
self.order.insert(thread, self.length);
|
||||||
self.selection.insert(thread, false);
|
self.selection.insert(thread, false);
|
||||||
|
self.length += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
min_width.0 = self.length.saturating_sub(1).to_string().len();
|
min_width.0 = self.length.saturating_sub(1).to_string().len();
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use super::default_vals::*;
|
use super::default_vals::*;
|
||||||
|
use crate::cache::Query;
|
||||||
|
|
||||||
/// Settings for mail listings
|
/// Settings for mail listings
|
||||||
#[derive(Debug, Deserialize, Clone, Default, Serialize)]
|
#[derive(Debug, Deserialize, Clone, Default, Serialize)]
|
||||||
|
@ -38,4 +39,9 @@ pub struct ListingSettings {
|
||||||
/// Default: true
|
/// Default: true
|
||||||
#[serde(default = "true_val")]
|
#[serde(default = "true_val")]
|
||||||
pub recent_dates: bool,
|
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