del notification, del systray, add i3blocks notify

This commit is contained in:
teldra 2019-08-26 01:52:58 +02:00
parent feb82747ef
commit a6a9598126
2 changed files with 52 additions and 121 deletions

View File

@ -24,11 +24,8 @@ debug=true
[dependencies] [dependencies]
imap = "1.0" imap = "1.0"
native-tls = "0.2" native-tls = "0.2"
systray = "0.3.0"
mailparse = "0.8" mailparse = "0.8"
toml = "0.5" toml = "0.5"
xdg = "2.1.0" xdg = "2.1.0"
notify-rust = "3.4.0"
rayon = "1.0.0" rayon = "1.0.0"
askama_escape = "0.2"
chrono = "0.4" chrono = "0.4"

View File

@ -1,11 +1,8 @@
extern crate askama_escape;
extern crate chrono; extern crate chrono;
extern crate imap; extern crate imap;
extern crate mailparse; extern crate mailparse;
extern crate native_tls; extern crate native_tls;
extern crate notify_rust;
extern crate rayon; extern crate rayon;
extern crate systray;
extern crate toml; extern crate toml;
extern crate xdg; extern crate xdg;
@ -21,6 +18,7 @@ use std::process::Command;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::io::Write;
#[derive(Clone)] #[derive(Clone)]
struct Account { struct Account {
@ -34,9 +32,10 @@ impl Account {
pub fn connect(&self) -> Result<Connection<TlsStream<TcpStream>>, imap::error::Error> { pub fn connect(&self) -> Result<Connection<TlsStream<TcpStream>>, imap::error::Error> {
let tls = TlsConnector::builder().build()?; let tls = TlsConnector::builder().build()?;
imap::connect((&*self.server.0, self.server.1), &self.server.0, &tls).and_then(|c| { imap::connect((&*self.server.0, self.server.1), &self.server.0, &tls).and_then(|c| {
let mut c = try!(c let mut c = try!(
.login(self.username.trim(), self.password.trim()) c.login(self.username.trim(), self.password.trim())
.map_err(|(e, _)| e)); .map_err(|(e, _)| e)
);
let cap = try!(c.capabilities()); let cap = try!(c.capabilities());
if !cap.iter().any(|&c| c == "IDLE") { if !cap.iter().any(|&c| c == "IDLE") {
return Err(imap::error::Error::Bad(cap.iter().cloned().collect())); return Err(imap::error::Error::Bad(cap.iter().cloned().collect()));
@ -62,7 +61,6 @@ impl<T: Read + Write + imap::extensions::idle::SetReadTimeout> Connection<T> {
// the connection has failed for some reason // the connection has failed for some reason
// try to log out (we probably can't) // try to log out (we probably can't)
eprintln!("connection to {} failed: {:?}", self.account.name, e); eprintln!("connection to {} failed: {:?}", self.account.name, e);
self.socket.logout().is_err();
break; break;
} }
} }
@ -96,7 +94,6 @@ impl<T: Read + Write + imap::extensions::idle::SetReadTimeout> Connection<T> {
) -> Result<(), imap::error::Error> { ) -> Result<(), imap::error::Error> {
// Keep track of all the e-mails we have already notified about // Keep track of all the e-mails we have already notified about
let mut last_notified = 0; let mut last_notified = 0;
let mut notification = None::<notify_rust::NotificationHandle>;
loop { loop {
// check current state of inbox // check current state of inbox
@ -111,8 +108,7 @@ impl<T: Read + Write + imap::extensions::idle::SetReadTimeout> Connection<T> {
let mut subjects = BTreeMap::new(); let mut subjects = BTreeMap::new();
if !uids.is_empty() { if !uids.is_empty() {
let uids: Vec<_> = uids.into_iter().map(|v: u32| format!("{}", v)).collect(); let uids: Vec<_> = uids.into_iter().map(|v: u32| format!("{}", v)).collect();
for msg in self for msg in self.socket
.socket
.uid_fetch(&uids.join(","), "RFC822.HEADER")? .uid_fetch(&uids.join(","), "RFC822.HEADER")?
.iter() .iter()
{ {
@ -158,46 +154,6 @@ impl<T: Read + Write + imap::extensions::idle::SetReadTimeout> Connection<T> {
} }
} }
if !subjects.is_empty() {
use notify_rust::{Notification, NotificationHint};
let title = format!(
"@{} has new mail ({} unseen)",
self.account.name, num_unseen
);
// we want the n newest e-mail in reverse chronological order
let mut body = String::new();
for subject in subjects.values().rev() {
body.push_str("> ");
body.push_str(subject);
body.push_str("\n");
}
let body = body.trim_end();
println!("! {}", title);
println!("{}", body);
if let Some(mut n) = notification.take() {
n.summary(&title).body(&format!(
"{}",
askama_escape::escape(body, askama_escape::Html)
));
n.update();
} else {
notification = Some(
Notification::new()
.summary(&title)
.body(&format!(
"{}",
askama_escape::escape(body, askama_escape::Html)
))
.icon("notification-message-email")
.hint(NotificationHint::Category("email.arrived".to_owned()))
.id(42) // for some reason, just updating isn't enough for dunst
.show()
.expect("failed to launch notify-send"),
);
}
}
tx.send((account, num_unseen)).unwrap(); tx.send((account, num_unseen)).unwrap();
@ -216,7 +172,7 @@ fn main() {
return; return;
} }
}; };
let config = match xdg.find_config_file("buzz.toml") { let config = match xdg.find_config_file("buzz/buzz.toml") {
Some(config) => config, Some(config) => config,
None => { None => {
println!("Could not find configuration file buzz.toml"); println!("Could not find configuration file buzz.toml");
@ -247,39 +203,40 @@ fn main() {
// Figure out what accounts we have to deal with // Figure out what accounts we have to deal with
let accounts: Vec<_> = match config.as_table() { let accounts: Vec<_> = match config.as_table() {
Some(t) => t Some(t) => {
.iter() t.iter()
.filter_map(|(name, v)| match v.as_table() { .filter_map(|(name, v)| match v.as_table() {
None => { None => {
println!("Configuration for account {} is broken: not a table", name); println!("Configuration for account {} is broken: not a table", name);
None None
} }
Some(t) => { Some(t) => {
let pwcmd = match t.get("pwcmd").and_then(|p| p.as_str()) { let pwcmd = match t.get("pwcmd").and_then(|p| p.as_str()) {
None => return None, None => return None,
Some(pwcmd) => pwcmd, Some(pwcmd) => pwcmd,
}; };
let password = match Command::new("sh").arg("-c").arg(pwcmd).output() { let password = match Command::new("sh").arg("-c").arg(pwcmd).output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).into_owned(), Ok(output) => String::from_utf8_lossy(&output.stdout).into_owned(),
Err(e) => { Err(e) => {
println!("Failed to launch password command for {}: {}", name, e); println!("Failed to launch password command for {}: {}", name, e);
return None; return None;
} }
}; };
Some(Account { Some(Account {
name: name.as_str().to_owned(), name: name.as_str().to_owned(),
server: ( server: (
t["server"].as_str().unwrap().to_owned(), t["server"].as_str().unwrap().to_owned(),
t["port"].as_integer().unwrap() as u16, t["port"].as_integer().unwrap() as u16,
), ),
username: t["username"].as_str().unwrap().to_owned(), username: t["username"].as_str().unwrap().to_owned(),
password: password, password: password,
}) })
} }
}) })
.collect(), .collect()
}
None => { None => {
println!("Could not parse configuration file buzz.toml: not a table"); println!("Could not parse configuration file buzz.toml: not a table");
return; return;
@ -291,25 +248,6 @@ fn main() {
return; return;
} }
// Create a new application
let mut app = match systray::Application::new() {
Ok(app) => app,
Err(e) => {
println!("Could not create gtk application: {}", e);
return;
}
};
if let Err(e) =
app.set_icon_from_file(&"/usr/share/icons/Faenza/stock/24/stock_disconnect.png".to_string())
{
println!("Could not set application icon: {}", e);
}
if let Err(e) = app.add_menu_item(&"Quit".to_string(), |window| {
window.quit();
}) {
println!("Could not add application Quit menu option: {}", e);
}
// TODO: w.set_tooltip(&"Whatever".to_string()); // TODO: w.set_tooltip(&"Whatever".to_string());
// TODO: app.wait_for_message(); // TODO: app.wait_for_message();
@ -323,7 +261,9 @@ fn main() {
Err(imap::error::Error::Io(e)) => { Err(imap::error::Error::Io(e)) => {
println!( println!(
"Failed to connect account {}: {}; retrying in {}s", "Failed to connect account {}: {}; retrying in {}s",
account.name, e, wait account.name,
e,
wait
); );
thread::sleep(Duration::from_secs(wait)); thread::sleep(Duration::from_secs(wait));
} }
@ -346,30 +286,24 @@ fn main() {
} }
// We have now connected // We have now connected
app.set_icon_from_file(&"/usr/share/icons/Faenza/stock/24/stock_connect.png".to_string())
.ok();
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut unseen: Vec<_> = accounts.iter().map(|_| 0).collect(); let mut unseen: Vec<_> = accounts.iter().map(|_| 0).collect();
for (i, conn) in accounts.into_iter().enumerate() { for (i, conn) in accounts.into_iter().enumerate() {
let tx = tx.clone(); let tx = tx.clone();
thread::spawn(move || { thread::spawn(move || { conn.handle(i, tx); });
conn.handle(i, tx);
});
} }
for (i, num_unseen) in rx { for (i, num_unseen) in rx {
unseen[i] = num_unseen; unseen[i] = num_unseen;
if unseen.iter().sum::<usize>() == 0 { let mut file = std::fs::File::create("/tmp/mails").expect("create failed");
app.set_icon_from_file( file.write_all(num_unseen.to_string().as_bytes()).expect(
&"/usr/share/icons/oxygen/base/32x32/status/mail-unread.png".to_string(), "write failed",
) );
.unwrap(); Command::new("pkill")
} else { .arg("-RTMIN+2")
app.set_icon_from_file( .arg("i3blocks")
&"/usr/share/icons/oxygen/base/32x32/status/mail-unread-new.png".to_string(), .spawn()
) .expect("pkill command failed to start");
.unwrap();
}
} }
} }