diff --git a/src/alert_email.rs b/src/alert_email.rs index 566fe5b..57fd01e 100644 --- a/src/alert_email.rs +++ b/src/alert_email.rs @@ -1,13 +1,54 @@ use crate::model::app_state::AppState; +use crate::model::config::AlertConfig; use crate::model::config::Config; use crate::model::config::SmtpMode; use clap::Parser; -use lettre::transport::smtp::authentication::Credentials; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use tracing::{info, warn}; -pub fn can_send_alerts() -> bool { - Config::parse().alert_config().is_some() +// pub fn alert_params_configured() -> bool { +// Config::parse().alert_config().is_some() +// } + +pub fn check_alert_params() -> bool { + match Config::parse().alert_config() { + Some(alert) => match gen_smtp_transport(&alert) { + Ok(_) => true, + Err(e) => { + warn!( + "invalid smtp parameters. alert emails disabled. error: {:?}", + e.to_string() + ); + false + } + }, + None => { + warn!("alert emails disabled. consider configuring smtp parameters."); + false + } + } +} + +pub fn gen_smtp_transport( + alert: &AlertConfig, +) -> Result, lettre::transport::smtp::Error> { + // corresponds to table at: + // https://docs.rs/lettre/0.11.7/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url + let (scheme, tls_arg) = match alert.smtp_mode { + SmtpMode::Smtps => ("smtps", ""), + SmtpMode::Starttls => ("smtp", "?tls=required"), + SmtpMode::Opportunistic => ("smtp", "?tls=opportunistic"), + SmtpMode::Plaintext => ("smtp", ""), + }; + + let user = &alert.smtp_user; + let pass = &alert.smtp_pass; + let host = &alert.smtp_host; + let port = alert.smtp_port; + + let smtp_url = format!("{scheme}://{user}:{pass}@{host}:{port}{tls_arg}"); + + Ok(AsyncSmtpTransport::::from_url(&smtp_url)?.build()) } pub async fn send( @@ -15,43 +56,33 @@ pub async fn send( subject: &str, body: String, ) -> std::result::Result { - match state.load().config.alert_config() { - None => { - warn!("Alert emails disabled. alert not sent. consider confiuring smtp parameters. subject: {subject}"); - Ok(false) + // this will log warnings if smtp not configured or mis-configured. + check_alert_params(); + + if let Some(alert) = state.load().config.alert_config() { + let email = Message::builder() + .from(alert.smtp_from_email.parse()?) + .to(alert.admin_email.parse()?) + .subject(subject) + .body(body)?; + + // note: this error case is already checked/logged by check_alert_params. + let mailer = gen_smtp_transport(&alert)?; + + // Attempt to send the email via the SMTP transport + match mailer.send(email).await { + Ok(_) => info!( + "alert email sent successfully. to: {}, subject: {subject}, smtp_host: {}:{}", + alert.admin_email, alert.smtp_host, alert.smtp_port + ), + Err(e) => warn!( + "error sending alert email. to: {}, subject: {subject}, smtp_host: {}:{}. error: {:?}", + alert.admin_email, alert.smtp_host, alert.smtp_port, e + ), } - Some(alert) => { - let email = Message::builder() - .from(alert.smtp_from_email.parse()?) - .to(alert.admin_email.parse()?) - .subject(subject) - .body(body)?; - // Create SMTP client credentials using username and password - let creds = Credentials::new(alert.smtp_user.to_string(), alert.smtp_pass.to_string()); - - // Open a secure connection to the SMTP server, possibly using STARTTLS - let relay = match alert.smtp_mode { - SmtpMode::Starttls => { - AsyncSmtpTransport::::starttls_relay(&alert.smtp_host)? - } - SmtpMode::Smtps => AsyncSmtpTransport::::relay(&alert.smtp_host)?, - }; - let mailer = relay.credentials(creds).build(); - - // Attempt to send the email via the SMTP transport - match mailer.send(email).await { - Ok(_) => info!( - "alert email sent successfully. to: {}, subject: {subject}, smtp_host: {}", - alert.admin_email, alert.smtp_host - ), - Err(e) => warn!( - "error send alert email. to: {}, subject: {subject}, smtp_host: {}. error: {:?}", - alert.admin_email, alert.smtp_host, e - ), - } - - Ok(true) - } + Ok(true) + } else { + Ok(false) } } diff --git a/src/main.rs b/src/main.rs index a0e7449..447164a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use neptune_explorer::rpc::block_digest::block_digest; use neptune_explorer::rpc::block_info::block_info; use neptune_explorer::rpc::utxo_digest::utxo_digest; use tower_http::services::ServeDir; -use tracing::{info, warn}; +use tracing::info; use tracing_subscriber::EnvFilter; #[tokio::main] @@ -33,9 +33,8 @@ async fn main() -> Result<(), anyhow::Error> { .await .with_context(|| format!("Failed to bind to port {port}"))?; - if !alert_email::can_send_alerts() { - warn!("alert emails disabled. consider configuring smtp parameters."); - } + // this will log warnings if smtp not configured or mis-configured. + alert_email::check_alert_params(); tokio::task::spawn(neptune_rpc::watchdog(app_state)); diff --git a/src/model/config.rs b/src/model/config.rs index 4d412ee..2e01e62 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -14,8 +14,8 @@ clap::ArgGroup::new("value") .required(false) .multiple(true) - .requires_all(&["admin_email", "smtp_host", "smtp_user", "smtp_pass", "smtp_from_email"]) - .args(&["admin_email", "smtp_host", "smtp_user", "smtp_pass", "smtp_from_email"]) + .requires_all(&["admin_email", "smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from_email"]) + .args(&["admin_email", "smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from_email"]) ))] pub struct Config { /// Sets the website name @@ -46,6 +46,10 @@ pub struct Config { #[arg(long, value_name = "host")] pub smtp_host: Option, + /// smtp port for alert emails + #[arg(long, value_name = "port", default_value = "25")] + pub smtp_port: Option, + /// smtp username for alert emails #[arg(long, value_name = "user")] pub smtp_user: Option, @@ -68,6 +72,7 @@ impl Config { match ( &self.admin_email, &self.smtp_host, + &self.smtp_port, &self.smtp_user, &self.smtp_pass, &self.smtp_from_email, @@ -76,6 +81,7 @@ impl Config { ( Some(admin_email), Some(smtp_host), + Some(smtp_port), Some(smtp_user), Some(smtp_pass), Some(smtp_from_email), @@ -83,6 +89,7 @@ impl Config { ) => Some(AlertConfig { admin_email: admin_email.clone(), smtp_host: smtp_host.clone(), + smtp_port: *smtp_port, smtp_user: smtp_user.clone(), smtp_pass: smtp_pass.clone(), smtp_from_email: smtp_from_email.clone(), @@ -101,6 +108,9 @@ pub struct AlertConfig { /// smtp host for alert emails pub smtp_host: String, + /// smtp host for alert emails + pub smtp_port: u16, + /// smtp username for alert emails pub smtp_user: String, @@ -115,10 +125,18 @@ pub struct AlertConfig { } #[derive(Debug, Clone, clap::ValueEnum)] +/// Determines SMTP encryption mode. +/// See: https://docs.rs/lettre/0.11.7/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url pub enum SmtpMode { /// smtps Smtps, - /// starttls + /// starttls required Starttls, + + /// use starttls if available. insecure. + Opportunistic, + + /// plain text. insecure. + Plaintext, }