From ef1a22d98649b87a37201f759a3f2c122ec27b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 7 May 2021 16:41:51 +0200 Subject: [PATCH] split idle cmd into notify+watch, remove err when empty result --- src/config/model.rs | 22 +++++---- src/imap/cli.rs | 46 +++++++++++++++++-- src/imap/model.rs | 106 +++++++++++++++++++++++++------------------- src/msg/cli.rs | 19 ++++++-- src/msg/model.rs | 15 ++++--- src/table.rs | 2 +- 6 files changed, 143 insertions(+), 67 deletions(-) diff --git a/src/config/model.rs b/src/config/model.rs index c36be06..31b7a33 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -28,6 +28,7 @@ pub struct Account { pub downloads_dir: Option, pub signature: Option, pub default_page_size: Option, + pub watch_cmds: Option>, // Specific pub default: Option, @@ -118,10 +119,7 @@ pub struct Config { pub notify_cmd: Option, pub signature: Option, pub default_page_size: Option, - - #[serde(default)] - pub idle_hook_cmds: Vec, - + pub watch_cmds: Option>, #[serde(flatten)] pub accounts: HashMap, } @@ -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); }) }); diff --git a/src/imap/cli.rs b/src/imap/cli.rs index 40c84da..e85632e 100644 --- a/src/imap/cli.rs +++ b/src/imap/cli.rs @@ -12,15 +12,53 @@ error_chain! { } pub fn imap_subcmds<'a>() -> Vec> { - 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 { - 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); diff --git a/src/imap/model.rs b/src/imap/model.rs index 75006ba..e3984f4 100644 --- a/src/imap/model.rs +++ b/src/imap/model.rs @@ -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>, } -impl<'ic> ImapConnector<'ic> { - pub fn new(account: &'ic Account) -> Result { +impl<'a> ImapConnector<'a> { + pub fn new(account: &'a Account) -> Result { 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 = @@ -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>> { + ) -> Result>>> { 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>> { + ) -> Result>>> { 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 = 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::>(); - 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::>(); + .chain_err(|| format!("Could not fetch range `{}`", &range))?; - Ok(fetches) + Ok(Some(fetches)) } pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result> { 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(()) } diff --git a/src/msg/cli.rs b/src/msg/cli.rs index 20facae..bec598a 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -187,7 +187,12 @@ pub fn msg_matches(app: &App) -> Result { 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 { 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 { 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(); diff --git a/src/msg/model.rs b/src/msg/model.rs index 5bc8d6c..1ff3325 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -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>); +pub struct Msgs<'a>(pub Vec>); -impl<'m> From<&'m imap::types::ZeroCopy>> for Msgs<'m> { - fn from(fetches: &'m imap::types::ZeroCopy>) -> Self { +impl<'a> From<&'a imap::types::ZeroCopy>> for Msgs<'a> { + fn from(fetches: &'a imap::types::ZeroCopy>) -> Self { Self(fetches.iter().rev().map(Msg::from).collect::>()) } } -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)) } diff --git a/src/table.rs b/src/table.rs index a1ee630..b1cc152 100644 --- a/src/table.rs +++ b/src/table.rs @@ -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::>() .join("\n") }