commit 55c6cbf79df0b49e557cab18d15ebdc952243d7d from: witcher date: Mon Dec 19 11:12:51 2022 UTC Enable config file attributes on command line All possible configuration file attributes can be given on the command line instead and, if all have been given, no configuration file is needed for the program to run. Implements: https://todo.sr.ht/~witcher/rss-email/24 References: https://todo.sr.ht/~witcher/rss-email/13 commit - 53515ec93f87219808a6d2bcbe9416f30d235cb0 commit + 55c6cbf79df0b49e557cab18d15ebdc952243d7d blob - 7044fa665cbe06e9d22808d70360750fb0cb0bb3 blob + c7387c964ec3e2b3c4f9e2e82c5e4e0a3ed9e432 --- docs/rss-email.1.scd +++ docs/rss-email.1.scd @@ -6,7 +6,7 @@ rss-email - Fetch new RSS feed items and send them via # SYNOPSIS -*rss-email* [--config ] [--database ] [--dry-run] [-h|--help] [--urls ] [-V|--version] +*rss-email* [OPTIONS] # DESCRIPTION @@ -22,21 +22,51 @@ The database used is *sqlite3*. The following options are recognized by *rss-email*: -*--config* _config-path_ +*--config* _path_ Specifies a custom configuration to be used instead of the default one at *$XDG_CONFIG_HOME/rss-email/config.toml* -*--database* _database-path_ +*--database* _path_ Specifies a custom database to be used instead of the default one at *$XDG_CONFIG_HOME/rss-email/cache.db* *--dry-run* Don't send any emails, just fetch new feed items and mark them as read -*--urls* _urls-path_ +*--urls* _path_ Specifies a custom urls file to be used instead of the default one at *$XDG_CONFIG_HOME/rss-email/urls* +*-h*, *--help* + Print help information + +*-V*, *--version* + Print version information + +The following options override the fields from the configuration file: + +*--mail-from* _from_ + Email "From" header + +*--mail-to* _to_ + Email "To" header + +*--smtp-password* _password_ + SMTP password + +*--smtp-port* _port_ + SMTP server port + +*--smtp-server* _server_ + SMTP server + +*--smtp-user* _user_ + SMTP user + +If all of these options are given, no configuration file will be read, and thus +no configuration file is necessary. Missing options will be read from the +configuration file. + # SEE ALSO *rss-email*(5) blob - 59acc67ba1e0fccebc9615d7ecea0568b2345dd8 blob + 22e79fd4bdbbb0fcda3f3e028af9eaa84b7be13d --- src/cli.rs +++ src/cli.rs @@ -17,6 +17,24 @@ pub struct Cli { /// Don't send emails #[clap(long, value_parser)] pub dry_run: bool, + /// Email "From" header + #[clap(long, value_name = "from", value_parser)] + pub mail_from: Option, + /// Email "To" header + #[clap(long, value_name = "to", value_parser)] + pub mail_to: Option, + /// SMTP user + #[clap(long, value_name = "user", value_parser)] + pub smtp_user: Option, + /// SMTP password + #[clap(long, value_name = "password", value_parser)] + pub smtp_password: Option, + /// SMTP server + #[clap(long, value_name = "server", value_parser)] + pub smtp_server: Option, + /// SMTP server port + #[clap(long, value_name = "port", value_parser = clap::value_parser!(u16).range(1..))] + pub smtp_port: Option, } impl Cli { blob - cfa54e844a67c1449635bd5b3e23cb5697a6cc2f blob + 1563451cab920c6f5c7f26af86242855400b1b4a --- src/config.rs +++ src/config.rs @@ -34,3 +34,61 @@ impl Config { Ok(config) } } + +/// The global configuration for the whole program. +/// +/// This is the global configuration struct. Instead of carrying around the command line arguments +/// and config file values, this struct holds both, overriding config file values with command line +/// ones. +#[derive(Debug, Default)] +pub struct AppConfig { + pub database_path: String, + pub urls_path: String, + pub dry_run: bool, + pub mail_from: String, + pub mail_to: String, + pub smtp_user: String, + pub smtp_password: String, + pub smtp_server: String, + pub smtp_port: u16, +} + +impl AppConfig { + pub fn new() -> anyhow::Result { + let cli = crate::cli::Cli::build_app()?; + let mut s = AppConfig { + database_path: cli.database_path, + urls_path: cli.urls_path, + dry_run: cli.dry_run, + ..Default::default() + }; + + // if all the values on the command line that correspond to config file options have a + // value given, there is no need to require a config file at all + if !(cli.mail_from.is_some() + && cli.mail_to.is_some() + && cli.smtp_user.is_some() + && cli.smtp_password.is_some() + && cli.smtp_server.is_some() + && cli.smtp_port.is_some()) + { + let cfg = Config::new(cli.config_path)?; + s.mail_from = cli.mail_from.unwrap_or(cfg.mail.from); + s.mail_to = cli.mail_to.unwrap_or(cfg.mail.to); + s.smtp_user = cli.smtp_user.unwrap_or(cfg.smtp.user); + s.smtp_password = cli.smtp_password.unwrap_or(cfg.smtp.password); + s.smtp_server = cli.smtp_server.unwrap_or(cfg.smtp.server); + s.smtp_port = cli.smtp_port.unwrap_or(cfg.smtp.port); + } else { + trace!("All necessary config values have been given on the command line, no need to check for config file"); + s.mail_from = cli.mail_from.unwrap(); + s.mail_to = cli.mail_to.unwrap(); + s.smtp_user = cli.smtp_user.unwrap(); + s.smtp_password = cli.smtp_password.unwrap(); + s.smtp_server = cli.smtp_server.unwrap(); + s.smtp_port = cli.smtp_port.unwrap(); + } + + Ok(s) + } +} blob - ce2e1334fdc800f008887b2094e3add8da6622ff blob + 7d5c380917d5269d733a659e5f8c13492a0b535d --- src/mail.rs +++ src/mail.rs @@ -1,4 +1,3 @@ -use crate::config::Config; use lettre::{ message::Message, transport::smtp::{authentication::Credentials, AsyncSmtpTransport}, @@ -26,15 +25,16 @@ impl Mail { Self { subject, body } } - pub async fn send_email( + pub async fn send_email<'a>( self, - config: &Config, + from: &'a str, + to: &'a str, mailer: &AsyncSmtpTransport, ) -> anyhow::Result<()> { - trace!("Sending to {}: {}", config.mail.to, &self.subject); + trace!("Sending to {}: {}", to, &self.subject); let email = Message::builder() - .from(config.mail.from.parse()?) - .to(config.mail.to.parse()?) + .from(from.parse()?) + .to(to.parse()?) .subject(self.subject) .body(self.body)?; @@ -44,15 +44,17 @@ impl Mail { } } -pub fn get_mailer(config: &Config) -> anyhow::Result> { - let creds = Credentials::new( - config.smtp.user.to_string(), - config.smtp.password.to_string(), - ); +pub fn get_mailer( + user: String, + password: String, + server: String, + port: u16, +) -> anyhow::Result> { + let creds = Credentials::new(user, password); - let mailer = AsyncSmtpTransport::::relay(&config.smtp.server)? + let mailer = AsyncSmtpTransport::::relay(&server)? .credentials(creds) - .port(config.smtp.port) + .port(port) .build(); Ok(mailer) blob - f499eba9670ab4caca50e3d9efa905bc984a8643 blob + 5ef21d457b08c806796978a19ea6229d0b3783eb --- src/main.rs +++ src/main.rs @@ -10,9 +10,11 @@ pub mod feed; pub mod mail; pub mod models; -use crate::mail::{get_mailer, Mail}; +use crate::{ + config::AppConfig, + mail::{get_mailer, Mail}, +}; use anyhow::Context; -use config::Config; use lettre::{AsyncSmtpTransport, Tokio1Executor}; use std::{ fs::File, @@ -25,19 +27,19 @@ use tokio::task::JoinSet; #[tokio::main] async fn main() -> anyhow::Result<()> { + // TODO: change to simple logger with verbosity specified by user on command line env_logger::init(); - let args = cli::Cli::build_app()?; - let config = Arc::new(Config::new(args.config_path)?); + let config = Arc::new(AppConfig::new()?); let urls = BufReader::new( - File::open(args.urls_path.as_str()) - .context(format!("File {:?} does not exist", &args.urls_path))?, + File::open(config.urls_path.as_str()) + .context(format!("File {:?} does not exist", &config.urls_path))?, ) .lines() .map(|l| l.unwrap()) .filter(|l| !l.starts_with('#')); - let db_path = args.database_path; + let db_path = &config.database_path; debug!("Establishing connection to database at {:?}", db_path); let pool = SqlitePoolOptions::new() .max_connections(5) @@ -70,15 +72,30 @@ async fn main() -> anyhow::Result<()> { .fetch_all(&pool) .await?; - let mailer = get_mailer(&config)?; + let mailer = get_mailer( + config.smtp_user.clone(), + config.smtp_password.clone(), + config.smtp_server.clone(), + config.smtp_port, + )?; + let mut handles = JoinSet::new(); for result in results { let mut conn = pool.acquire().await?; let mailer = mailer.clone(); let config = config.clone(); - handles - .spawn(async move { send_post(&mut conn, mailer, config, result, args.dry_run).await }); + handles.spawn(async move { + send_post( + &mut conn, + mailer, + &config.mail_from, + &config.mail_to, + result, + config.dry_run, + ) + .await + }); } while let Some(handle) = handles.join_next().await { @@ -94,7 +111,8 @@ async fn main() -> anyhow::Result<()> { async fn send_post<'a, E>( conn: E, mailer: AsyncSmtpTransport, - config: Arc, + from: &'a str, + to: &'a str, post: models::Post, dry_run: bool, ) -> anyhow::Result<()> @@ -103,7 +121,7 @@ where { if !dry_run { Mail::new(post.title, post.content, post.url) - .send_email(&config, &mailer) + .send_email(from, to, &mailer) .await?; }