diff --git a/Cargo.lock b/Cargo.lock index 3b4783d..38f51b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,12 +19,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "bitflags" version = "1.3.2" @@ -86,22 +80,6 @@ dependencies = [ "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]] name = "heck" version = "0.4.0" @@ -124,7 +102,6 @@ dependencies = [ "anyhow", "clap", "rustls", - "rustls-native-certs", "webpki-roots", ] @@ -137,12 +114,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.137" @@ -164,12 +135,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - [[package]] name = "os_str_bytes" version = "6.4.0" @@ -245,37 +210,6 @@ dependencies = [ "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]] name = "sct" version = "0.7.0" @@ -286,29 +220,6 @@ dependencies = [ "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]] name = "spin" version = "0.5.2" @@ -472,46 +383,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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" diff --git a/Cargo.toml b/Cargo.toml index 899176b..e4ea15b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,4 @@ edition = "2021" anyhow = "1.0.66" clap = { version = "4.0.26", features = ["derive"] } rustls = "0.20.7" -rustls-native-certs = "0.6.2" webpki-roots = "0.22.5" diff --git a/src/lib.rs b/src/lib.rs index 3cee5b4..c0ae85b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,13 @@ use rustls::{OwnedTrustAnchor, ClientConfig, RootCertStore, ClientConnection}; -use anyhow::{Result as AResult, anyhow}; -use std::sync::Arc; -use std::net::TcpStream; -use std::io::{Read, Write}; +use anyhow::{Result as AResult, bail}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::io::{self, ErrorKind, Read, Write}; use std::process::Command; use std::time::Duration; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::mem; use clap::Parser; #[derive(Parser)] @@ -27,10 +29,15 @@ pub struct Cli { #[arg(short, long)] password: String, + /// interval (in seconds) at which to run even if no email arrives + #[arg(short, long)] + interval: Option, + /// command to run when new mail arrives #[arg(short, long)] command: PathBuf, + /// show all server responses #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, } @@ -44,6 +51,26 @@ enum State { } 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() .with_safe_defaults() .with_root_certificates(RootCertStore { @@ -54,12 +81,16 @@ pub fn run(cli: &Cli) -> AResult<()> { }) .with_no_client_auth(); - let mut buffer = [0; 2048]; + let mut buffer = [0u8; 2048]; let mut tls_client = ClientConnection::new( Arc::new(tls_config), 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::>(); + let mut socket = TcpStream::connect(addrs.as_slice())?; let mut state = State::Unauthenticated; socket.set_read_timeout(Some(Duration::from_secs(10*60)))?; @@ -94,6 +125,12 @@ pub fn run(cli: &Cli) -> AResult<()> { for response in responses { 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)); } @@ -107,20 +144,24 @@ pub fn run(cli: &Cli) -> AResult<()> { tls_client.writer().write(b"A002 select inbox\r\n")?; state = State::Inbox; } 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") { tls_client.writer().write(b"A003 idle\r\n")?; state = State::Idling; } 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") { println!("Connected and idling ..."); } 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()) - .output()?; + .output() + .expect("command execution failed"); + println!("Command finished."); + mem::drop(lock); } } } @@ -132,3 +173,13 @@ pub fn run(cli: &Cli) -> AResult<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dns_lookup() { + + } +} diff --git a/src/main.rs b/src/main.rs index 75f16cb..bc64ccd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,17 @@ use std::thread; use std::time::Duration; 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::ConnectionReset, + ErrorKind::NotConnected, ErrorKind::NetworkUnreachable, - ErrorKind::HostUnreachable + ErrorKind::HostUnreachable, ]; fn main() -> AResult<()> { @@ -20,13 +26,13 @@ fn main() -> AResult<()> { return match imapidle::run(&cli) { Ok(_) => Ok(()), Err(err) => match err.downcast_ref::() { - 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; - 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)); 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; println!("Cannot connect currently, retrying in {secs_to_reconnect} seconds"); thread::sleep(Duration::from_secs(secs_to_reconnect));