del notification, del systray, add i3blocks notify
This commit is contained in:
parent
feb82747ef
commit
a6a9598126
|
@ -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"
|
||||||
|
|
170
src/main.rs
170
src/main.rs
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue