split idle cmd into notify+watch, remove err when empty result

This commit is contained in:
Clément DOUIN 2021-05-07 16:41:51 +02:00
parent 9d40a7d30f
commit ef1a22d986
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
6 changed files with 143 additions and 67 deletions

View file

@ -28,6 +28,7 @@ pub struct Account {
pub downloads_dir: Option<PathBuf>,
pub signature: Option<String>,
pub default_page_size: Option<usize>,
pub watch_cmds: Option<Vec<String>>,
// Specific
pub default: Option<bool>,
@ -118,10 +119,7 @@ pub struct Config {
pub notify_cmd: Option<String>,
pub signature: Option<String>,
pub default_page_size: Option<usize>,
#[serde(default)]
pub idle_hook_cmds: Vec<String>,
pub watch_cmds: Option<Vec<String>>,
#[serde(flatten)]
pub accounts: HashMap<String, Account>,
}
@ -246,15 +244,21 @@ impl Config {
.to_owned()
}
pub fn exec_idle_hooks(&self) -> Result<()> {
let cmds = self.idle_hook_cmds.to_owned();
pub fn exec_watch_cmds(&self, account: &Account) -> Result<()> {
let cmds = account
.watch_cmds
.as_ref()
.or_else(|| self.watch_cmds.as_ref())
.map(|cmds| cmds.to_owned())
.unwrap_or_default();
debug!("cmds: {:?}", cmds);
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("execute cmd {:?}", cmd);
let res = run_cmd(&cmd);
debug!("res: {:?}", res);
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});

View file

@ -12,15 +12,53 @@ error_chain! {
}
pub fn imap_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("idle").about("Spawns a blocking idle daemon")]
vec![
clap::SubCommand::with_name("notify")
.about("Notifies when new messages arrive in the given mailbox")
.aliases(&["idle"])
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
clap::SubCommand::with_name("watch")
.about("Watches IMAP server changes")
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
]
}
pub fn imap_matches(app: &App) -> Result<bool> {
if let Some(_) = app.arg_matches.subcommand_matches("idle") {
debug!("idle command matched");
if let Some(matches) = app.arg_matches.subcommand_matches("notify") {
debug!("notify command matched");
let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64);
debug!("keepalive: {}", &keepalive);
let mut imap_conn = ImapConnector::new(&app.account)?;
imap_conn.idle(&app.config, &app.mbox)?;
imap_conn.notify(&app, keepalive)?;
imap_conn.logout();
return Ok(true);
}
if let Some(matches) = app.arg_matches.subcommand_matches("watch") {
debug!("watch command matched");
let keepalive = clap::value_t_or_exit!(matches.value_of("keepalive"), u64);
debug!("keepalive: {}", &keepalive);
let mut imap_conn = ImapConnector::new(&app.account)?;
imap_conn.watch(&app, keepalive)?;
imap_conn.logout();
return Ok(true);

View file

@ -4,11 +4,7 @@ use log::{debug, trace};
use native_tls::{self, TlsConnector, TlsStream};
use std::{collections::HashSet, iter::FromIterator, net::TcpStream};
use crate::{
config::model::{Account, Config},
flag::model::Flag,
msg::model::Msg,
};
use crate::{app::App, config::model::Account, flag::model::Flag, msg::model::Msg};
error_chain! {
links {
@ -22,30 +18,30 @@ pub struct ImapConnector<'a> {
pub sess: imap::Session<TlsStream<TcpStream>>,
}
impl<'ic> ImapConnector<'ic> {
pub fn new(account: &'ic Account) -> Result<Self> {
impl<'a> ImapConnector<'a> {
pub fn new(account: &'a Account) -> Result<Self> {
debug!("create TLS builder");
let insecure = account.imap_insecure();
let tls = TlsConnector::builder()
.danger_accept_invalid_certs(insecure)
.danger_accept_invalid_hostnames(insecure)
.build()
.chain_err(|| "Cannot create TLS connector")?;
.chain_err(|| "Could not create TLS connector")?;
debug!("create client");
let client = if account.imap_starttls() {
imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls)
.chain_err(|| "Cannot connect using STARTTLS")
.chain_err(|| "Could not connect using STARTTLS")
} else {
imap::connect(account.imap_addr(), &account.imap_host, &tls)
.chain_err(|| "Cannot connect using TLS")
.chain_err(|| "Could not connect using TLS")
}?;
debug!("create session");
let sess = client
.login(&account.imap_login, &account.imap_passwd()?)
.map_err(|res| res.0)
.chain_err(|| "Cannot login to IMAP server")?;
.chain_err(|| "Could not login to IMAP server")?;
Ok(Self { account, sess })
}
@ -60,11 +56,11 @@ impl<'ic> ImapConnector<'ic> {
pub fn set_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
self.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
self.sess
.uid_store(uid_seq, format!("FLAGS ({})", flags))
.chain_err(|| format!("Cannot set flags `{}`", &flags))?;
.chain_err(|| format!("Could not set flags `{}`", &flags))?;
Ok(())
}
@ -72,11 +68,11 @@ impl<'ic> ImapConnector<'ic> {
pub fn add_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
self.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
self.sess
.uid_store(uid_seq, format!("+FLAGS ({})", flags))
.chain_err(|| format!("Cannot add flags `{}`", &flags))?;
.chain_err(|| format!("Could not add flags `{}`", &flags))?;
Ok(())
}
@ -84,11 +80,11 @@ impl<'ic> ImapConnector<'ic> {
pub fn remove_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
self.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
self.sess
.uid_store(uid_seq, format!("-FLAGS ({})", flags))
.chain_err(|| format!("Cannot remove flags `{}`", &flags))?;
.chain_err(|| format!("Could not remove flags `{}`", &flags))?;
Ok(())
}
@ -106,11 +102,11 @@ impl<'ic> ImapConnector<'ic> {
Ok(uids)
}
pub fn idle(&mut self, config: &Config, mbox: &str) -> Result<()> {
debug!("examine mailbox: {}", mbox);
pub fn notify(&mut self, app: &App, keepalive: u64) -> Result<()> {
debug!("examine mailbox: {}", &app.mbox);
self.sess
.examine(mbox)
.chain_err(|| format!("Could not examine mailbox `{}`", mbox))?;
.examine(&app.mbox)
.chain_err(|| format!("Could not examine mailbox `{}`", &app.mbox))?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> =
@ -122,7 +118,7 @@ impl<'ic> ImapConnector<'ic> {
self.sess
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(300, 0));
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive()
})
.chain_err(|| "Could not start the idle mode")?;
@ -151,7 +147,7 @@ impl<'ic> ImapConnector<'ic> {
let uid = fetch.uid.ok_or_else(|| {
format!("Could not retrieve message {}'s UID", fetch.message)
})?;
config.run_notify_cmd(&msg.subject, &msg.sender)?;
app.config.run_notify_cmd(&msg.subject, &msg.sender)?;
debug!("notify message: {}", uid);
trace!("message: {:?}", msg);
@ -161,7 +157,26 @@ impl<'ic> ImapConnector<'ic> {
}
}
config.exec_idle_hooks()?;
debug!("end loop");
}
}
pub fn watch(&mut self, app: &App, keepalive: u64) -> Result<()> {
debug!("examine mailbox: {}", &app.mbox);
self.sess
.examine(&app.mbox)
.chain_err(|| format!("Could not examine mailbox `{}`", &app.mbox))?;
loop {
debug!("begin loop");
self.sess
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive()
})
.chain_err(|| "Could not start the idle mode")?;
app.config.exec_watch_cmds(&app.account)?;
debug!("end loop");
}
}
@ -170,7 +185,7 @@ impl<'ic> ImapConnector<'ic> {
let names = self
.sess
.list(Some(""), Some("*"))
.chain_err(|| "Cannot list mailboxes")?;
.chain_err(|| "Could not list mailboxes")?;
Ok(names)
}
@ -180,15 +195,15 @@ impl<'ic> ImapConnector<'ic> {
mbox: &str,
page_size: &usize,
page: &usize,
) -> Result<imap::types::ZeroCopy<Vec<imap::types::Fetch>>> {
) -> Result<Option<imap::types::ZeroCopy<Vec<imap::types::Fetch>>>> {
let last_seq = self
.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?
.exists as i64;
if last_seq == 0 {
return Err(format!("The `{}` mailbox is empty", mbox).into());
return Ok(None);
}
// TODO: add tests, improve error management when empty page
@ -204,9 +219,9 @@ impl<'ic> ImapConnector<'ic> {
let fetches = self
.sess
.fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)")
.chain_err(|| "Cannot fetch messages")?;
.chain_err(|| "Could not fetch messages")?;
Ok(fetches)
Ok(Some(fetches))
}
pub fn search_msgs(
@ -215,45 +230,46 @@ impl<'ic> ImapConnector<'ic> {
query: &str,
page_size: &usize,
page: &usize,
) -> Result<imap::types::ZeroCopy<Vec<imap::types::Fetch>>> {
) -> Result<Option<imap::types::ZeroCopy<Vec<imap::types::Fetch>>>> {
self.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
let begin = page * page_size;
let end = begin + (page_size - 1);
let uids = self
let uids: Vec<String> = self
.sess
.search(query)
.chain_err(|| format!("Cannot search in `{}` with query `{}`", mbox, query))?
.chain_err(|| format!("Could not search in `{}` with query `{}`", mbox, query))?
.iter()
.map(|seq| seq.to_string())
.collect::<Vec<_>>();
let range = uids[begin..end.min(uids.len())].join(",");
.collect();
if uids.is_empty() {
return Ok(None);
}
let range = uids[begin..end.min(uids.len())].join(",");
let fetches = self
.sess
.fetch(&range, "(UID FLAGS ENVELOPE INTERNALDATE)")
.chain_err(|| format!("Cannot fetch range `{}`", &range))?;
// .iter()
// .map(|fetch| Msg::from(fetch))
// .collect::<Vec<_>>();
.chain_err(|| format!("Could not fetch range `{}`", &range))?;
Ok(fetches)
Ok(Some(fetches))
}
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {
self.sess
.select(mbox)
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
match self
.sess
.uid_fetch(uid, "(FLAGS BODY[])")
.chain_err(|| "Cannot fetch bodies")?
.chain_err(|| "Could not fetch bodies")?
.first()
{
None => Err(format!("Cannot find message `{}`", uid).into()),
None => Err(format!("Could not find message `{}`", uid).into()),
Some(fetch) => Ok(fetch.body().unwrap_or(&[]).to_vec()),
}
}
@ -261,7 +277,7 @@ impl<'ic> ImapConnector<'ic> {
pub fn append_msg(&mut self, mbox: &str, msg: &[u8], flags: &[Flag]) -> Result<()> {
self.sess
.append_with_flags(mbox, msg, flags)
.chain_err(|| format!("Cannot append message to `{}`", mbox))?;
.chain_err(|| format!("Could not append message to `{}`", mbox))?;
Ok(())
}

View file

@ -187,7 +187,12 @@ pub fn msg_matches(app: &App) -> Result<bool> {
let mut imap_conn = ImapConnector::new(&app.account)?;
let msgs = imap_conn.list_msgs(&app.mbox, &page_size, &page)?;
let msgs = Msgs::from(&msgs);
let msgs = if let Some(ref fetches) = msgs {
Msgs::from(fetches)
} else {
Msgs::new()
};
trace!("messages: {:?}", msgs);
app.output.print(msgs);
@ -238,7 +243,11 @@ pub fn msg_matches(app: &App) -> Result<bool> {
let mut imap_conn = ImapConnector::new(&app.account)?;
let msgs = imap_conn.search_msgs(&app.mbox, &query, &page_size, &page)?;
let msgs = Msgs::from(&msgs);
let msgs = if let Some(ref fetches) = msgs {
Msgs::from(fetches)
} else {
Msgs::new()
};
trace!("messages: {:?}", msgs);
app.output.print(msgs);
@ -637,7 +646,11 @@ pub fn msg_matches(app: &App) -> Result<bool> {
let mut imap_conn = ImapConnector::new(&app.account)?;
let msgs =
imap_conn.list_msgs(&app.mbox, &app.config.default_page_size(&app.account), &0)?;
let msgs = Msgs::from(&msgs);
let msgs = if let Some(ref fetches) = msgs {
Msgs::from(fetches)
} else {
Msgs::new()
};
app.output.print(msgs);
imap_conn.logout();

View file

@ -80,7 +80,6 @@ impl<'a> Attachments {
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
if !ctype.starts_with("text") {
self.0.push(Attachment::from_part(part));
}
@ -646,15 +645,21 @@ impl<'m> Table for Msg<'m> {
// Msgs
#[derive(Debug, Serialize)]
pub struct Msgs<'m>(pub Vec<Msg<'m>>);
pub struct Msgs<'a>(pub Vec<Msg<'a>>);
impl<'m> From<&'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>> for Msgs<'m> {
fn from(fetches: &'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>) -> Self {
impl<'a> From<&'a imap::types::ZeroCopy<Vec<imap::types::Fetch>>> for Msgs<'a> {
fn from(fetches: &'a imap::types::ZeroCopy<Vec<imap::types::Fetch>>) -> Self {
Self(fetches.iter().rev().map(Msg::from).collect::<Vec<_>>())
}
}
impl<'m> fmt::Display for Msgs<'m> {
impl Msgs<'_> {
pub fn new() -> Self {
Self(vec![])
}
}
impl fmt::Display for Msgs<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "\n{}", Table::render(&self.0))
}

View file

@ -256,7 +256,7 @@ where
fn render(items: &[Self]) -> String {
Self::build(items)
.iter()
.map(|row| row.join(&Cell::new("|").ext(8).to_string()))
.map(|row| row.join(&Cell::new("").ext(8).to_string()))
.collect::<Vec<_>>()
.join("\n")
}