better error handling, intervals

This commit is contained in:
Florian Stecker 2022-12-03 17:24:05 -05:00
parent 4a22e30467
commit ec46c3b608
4 changed files with 72 additions and 148 deletions

132
Cargo.lock generated
View File

@ -19,12 +19,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -86,22 +80,6 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.0" version = "0.4.0"
@ -124,7 +102,6 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"rustls", "rustls",
"rustls-native-certs",
"webpki-roots", "webpki-roots",
] ]
@ -137,12 +114,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.137"
@ -164,12 +135,6 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.4.0" version = "6.4.0"
@ -245,37 +210,6 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [
"base64",
]
[[package]]
name = "schannel"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
]
[[package]] [[package]]
name = "sct" name = "sct"
version = "0.7.0" version = "0.7.0"
@ -286,29 +220,6 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "security-framework"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.5.2" version = "0.5.2"
@ -472,46 +383,3 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"

View File

@ -9,5 +9,4 @@ edition = "2021"
anyhow = "1.0.66" anyhow = "1.0.66"
clap = { version = "4.0.26", features = ["derive"] } clap = { version = "4.0.26", features = ["derive"] }
rustls = "0.20.7" rustls = "0.20.7"
rustls-native-certs = "0.6.2"
webpki-roots = "0.22.5" webpki-roots = "0.22.5"

View File

@ -1,11 +1,13 @@
use rustls::{OwnedTrustAnchor, ClientConfig, RootCertStore, ClientConnection}; use rustls::{OwnedTrustAnchor, ClientConfig, RootCertStore, ClientConnection};
use anyhow::{Result as AResult, anyhow}; use anyhow::{Result as AResult, bail};
use std::sync::Arc; use std::net::{TcpStream, ToSocketAddrs};
use std::net::TcpStream; use std::io::{self, ErrorKind, Read, Write};
use std::io::{Read, Write};
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::mem;
use clap::Parser; use clap::Parser;
#[derive(Parser)] #[derive(Parser)]
@ -27,10 +29,15 @@ pub struct Cli {
#[arg(short, long)] #[arg(short, long)]
password: String, password: String,
/// interval (in seconds) at which to run even if no email arrives
#[arg(short, long)]
interval: Option<u64>,
/// command to run when new mail arrives /// command to run when new mail arrives
#[arg(short, long)] #[arg(short, long)]
command: PathBuf, command: PathBuf,
/// show all server responses
#[arg(short, long, action = clap::ArgAction::Count)] #[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8, verbose: u8,
} }
@ -44,6 +51,26 @@ enum State {
} }
pub fn run(cli: &Cli) -> AResult<()> { pub fn run(cli: &Cli) -> AResult<()> {
// a mutex to avoid running the command concurrently
let mutex_mainthread = Arc::new(Mutex::new(()));
let mutex_subthread = Arc::clone(&mutex_mainthread);
if let Some(interval) = cli.interval {
let cmd = cli.command.clone();
thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(interval));
let lock = mutex_subthread.lock().unwrap();
println!("Interval timer expired, running command ...");
Command::new(cmd.as_os_str())
.output()
.expect("command execution failed");
println!("Command finished.");
mem::drop(lock);
}
});
}
let tls_config = ClientConfig::builder() let tls_config = ClientConfig::builder()
.with_safe_defaults() .with_safe_defaults()
.with_root_certificates(RootCertStore { .with_root_certificates(RootCertStore {
@ -54,12 +81,16 @@ pub fn run(cli: &Cli) -> AResult<()> {
}) })
.with_no_client_auth(); .with_no_client_auth();
let mut buffer = [0; 2048]; let mut buffer = [0u8; 2048];
let mut tls_client = ClientConnection::new( let mut tls_client = ClientConnection::new(
Arc::new(tls_config), Arc::new(tls_config),
cli.server.as_str().try_into().unwrap())?; cli.server.as_str().try_into().unwrap())?;
let mut socket = TcpStream::connect((cli.server.as_str(), cli.port))?; let addrs = (cli.server.as_str(), cli.port)
.to_socket_addrs()
.map_err(|e|io::Error::new(ErrorKind::NotConnected, e.to_string()))?
.collect::<Vec<_>>();
let mut socket = TcpStream::connect(addrs.as_slice())?;
let mut state = State::Unauthenticated; let mut state = State::Unauthenticated;
socket.set_read_timeout(Some(Duration::from_secs(10*60)))?; socket.set_read_timeout(Some(Duration::from_secs(10*60)))?;
@ -94,6 +125,12 @@ pub fn run(cli: &Cli) -> AResult<()> {
for response in responses { for response in responses {
if cli.verbose > 0 { if cli.verbose > 0 {
if state == State::Unauthenticated {
if let Some(suite) = tls_client.negotiated_cipher_suite() {
println!("negotiated cipher suite: {:?}", suite);
}
}
println!("{}", String::from_utf8_lossy(response)); println!("{}", String::from_utf8_lossy(response));
} }
@ -107,20 +144,24 @@ pub fn run(cli: &Cli) -> AResult<()> {
tls_client.writer().write(b"A002 select inbox\r\n")?; tls_client.writer().write(b"A002 select inbox\r\n")?;
state = State::Inbox; state = State::Inbox;
} else if response.starts_with(b"A001") { } else if response.starts_with(b"A001") {
return Err(anyhow!("The server rejected authentication")); bail!("The server rejected authentication");
}, },
State::Inbox => if response.starts_with(b"A002 OK") { State::Inbox => if response.starts_with(b"A002 OK") {
tls_client.writer().write(b"A003 idle\r\n")?; tls_client.writer().write(b"A003 idle\r\n")?;
state = State::Idling; state = State::Idling;
} else if response.starts_with(b"A002") { } else if response.starts_with(b"A002") {
return Err(anyhow!("Selecting inbox failed")); bail!("Selecting inbox failed");
}, },
State::Idling => if response.starts_with(b"+ idling") { State::Idling => if response.starts_with(b"+ idling") {
println!("Connected and idling ..."); println!("Connected and idling ...");
} else if response.starts_with(b"*") && response.ends_with(b"EXISTS") { } else if response.starts_with(b"*") && response.ends_with(b"EXISTS") {
println!("NEW EMAIL!"); let lock = mutex_mainthread.lock().unwrap();
println!("New email, running command ...");
Command::new(cli.command.as_os_str()) Command::new(cli.command.as_os_str())
.output()?; .output()
.expect("command execution failed");
println!("Command finished.");
mem::drop(lock);
} }
} }
} }
@ -132,3 +173,13 @@ pub fn run(cli: &Cli) -> AResult<()> {
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dns_lookup() {
}
}

View File

@ -6,11 +6,17 @@ use std::thread;
use std::time::Duration; use std::time::Duration;
use clap::Parser; use clap::Parser;
const CONNECTION_ERRORS: &[ErrorKind] = &[ const CONNECTION_LOST_ERRORS: &[ErrorKind] = &[
ErrorKind::Interrupted,
ErrorKind::WouldBlock, // when a read times out
];
const CANT_CONNECT_ERRORS: &[ErrorKind] = &[
ErrorKind::ConnectionAborted, ErrorKind::ConnectionAborted,
ErrorKind::ConnectionReset, ErrorKind::ConnectionReset,
ErrorKind::NotConnected,
ErrorKind::NetworkUnreachable, ErrorKind::NetworkUnreachable,
ErrorKind::HostUnreachable ErrorKind::HostUnreachable,
]; ];
fn main() -> AResult<()> { fn main() -> AResult<()> {
@ -20,13 +26,13 @@ fn main() -> AResult<()> {
return match imapidle::run(&cli) { return match imapidle::run(&cli) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => match err.downcast_ref::<IOError>() { Err(err) => match err.downcast_ref::<IOError>() {
Some(io_err) if io_err.kind() == ErrorKind::WouldBlock => { Some(io_err) if CONNECTION_LOST_ERRORS.contains(&io_err.kind()) => {
let secs_to_reconnect = 10; let secs_to_reconnect = 10;
println!("Timed out, reconnecting in {secs_to_reconnect} seconds"); println!("Connection lost, reconnecting in {secs_to_reconnect} seconds");
thread::sleep(Duration::from_secs(secs_to_reconnect)); thread::sleep(Duration::from_secs(secs_to_reconnect));
continue; continue;
}, },
Some(io_err) if CONNECTION_ERRORS.contains(&io_err.kind()) => { Some(io_err) if CANT_CONNECT_ERRORS.contains(&io_err.kind()) => {
let secs_to_reconnect = 10*60; let secs_to_reconnect = 10*60;
println!("Cannot connect currently, retrying in {secs_to_reconnect} seconds"); println!("Cannot connect currently, retrying in {secs_to_reconnect} seconds");
thread::sleep(Duration::from_secs(secs_to_reconnect)); thread::sleep(Duration::from_secs(secs_to_reconnect));