use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use smtp_test_tool::config::{default_save_path, discover_config_path, Config};
use smtp_test_tool::{outlook_defaults, run_tests, Profile};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use tracing_subscriber::EnvFilter;
#[derive(Parser, Debug)]
#[command(
name = "smtp-test-tool",
version,
about,
long_about = "Test SMTP / IMAP / POP3 connectivity to any mail server.\n\
Defaults to Outlook.com / Office 365."
)]
struct Cli {
#[arg(short, long, env = "SMTP_TEST_TOOL_CONFIG")]
config: Option<PathBuf>,
#[arg(short, long)]
profile: Option<String>,
#[arg(short, long)]
user: Option<String>,
#[arg(short = 'P', long)]
password: Option<String>,
#[arg(long)]
oauth_token: Option<String>,
#[arg(long)]
insecure: bool,
#[arg(long)]
log_level: Option<String>,
#[command(subcommand)]
cmd: Option<Cmd>,
}
#[derive(Subcommand, Debug)]
enum Cmd {
Test,
Profiles,
Init {
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn main() -> ExitCode {
match run() {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::from(1),
Err(e) => {
eprintln!("error: {e:#}");
ExitCode::from(2)
}
}
}
fn run() -> Result<bool> {
let cli = Cli::parse();
let lvl = cli.log_level.clone().unwrap_or_else(|| "info".into());
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&lvl));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_level(true)
.with_ansi(supports_colour())
.with_writer(io::stderr)
.init();
let cfg_path = cli.config.clone().or_else(discover_config_path);
let cfg = match &cfg_path {
Some(p) => Config::load(p).with_context(|| format!("loading {}", p.display()))?,
None => Config {
active: "default".into(),
profiles: [("default".into(), outlook_defaults())]
.into_iter()
.collect(),
},
};
let profile_name = cli.profile.clone().unwrap_or_else(|| cfg.active.clone());
match cli.cmd.unwrap_or(Cmd::Test) {
Cmd::Profiles => {
match &cfg_path {
Some(p) => println!("Profiles in {}:", p.display()),
None => println!("No config file loaded; using built-in defaults."),
}
for n in cfg.profile_names() {
println!(" {n}{}", if n == cfg.active { " (active)" } else { "" });
}
return Ok(true);
}
Cmd::Init { output } => {
let mut new_cfg = Config {
active: "default".into(),
profiles: Default::default(),
};
new_cfg.upsert_profile("default", outlook_defaults());
let target = output.unwrap_or_else(default_save_path);
new_cfg.save(&target)?;
println!("Wrote starter config to {}", target.display());
return Ok(true);
}
Cmd::Test => { }
}
let mut profile: Profile = cfg
.profile(&profile_name)
.cloned()
.unwrap_or_else(outlook_defaults);
if let Some(u) = cli.user {
profile.user = Some(u);
}
if let Some(p) = cli.password {
profile.password = Some(p);
}
if let Some(t) = cli.oauth_token {
profile.oauth_token = Some(t);
}
if cli.insecure {
profile.insecure_tls = true;
}
if profile.user.is_none() {
profile.user = Some(prompt("Username / email: ")?);
}
if profile.password.is_none() && profile.oauth_token.is_none() {
profile.password = Some(prompt_password("Password: ")?);
}
let results = run_tests(&profile);
Ok(results.all_passed())
}
fn prompt(msg: &str) -> Result<String> {
print!("{msg}");
io::stdout().flush().ok();
let mut s = String::new();
io::stdin().read_line(&mut s)?;
Ok(s.trim().to_string())
}
fn prompt_password(msg: &str) -> Result<String> {
eprint!("{msg}");
io::stderr().flush().ok();
let mut s = String::new();
io::stdin().read_line(&mut s)?;
Ok(s.trim_end_matches(['\r', '\n']).to_string())
}
fn supports_colour() -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
use std::io::IsTerminal;
io::stderr().is_terminal()
}