Commit Diff


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 <config-path>] [--database <database-path>] [--dry-run] [-h|--help] [--urls <urls-path>] [-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<String>,
+    /// Email "To" header
+    #[clap(long, value_name = "to", value_parser)]
+    pub mail_to: Option<String>,
+    /// SMTP user
+    #[clap(long, value_name = "user", value_parser)]
+    pub smtp_user: Option<String>,
+    /// SMTP password
+    #[clap(long, value_name = "password", value_parser)]
+    pub smtp_password: Option<String>,
+    /// SMTP server
+    #[clap(long, value_name = "server", value_parser)]
+    pub smtp_server: Option<String>,
+    /// SMTP server port
+    #[clap(long, value_name = "port", value_parser = clap::value_parser!(u16).range(1..))]
+    pub smtp_port: Option<u16>,
 }
 
 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<Self> {
+        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<Tokio1Executor>,
     ) -> 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<AsyncSmtpTransport<Tokio1Executor>> {
-    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<AsyncSmtpTransport<Tokio1Executor>> {
+    let creds = Credentials::new(user, password);
 
-    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&config.smtp.server)?
+    let mailer = AsyncSmtpTransport::<Tokio1Executor>::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<Tokio1Executor>,
-    config: Arc<Config>,
+    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?;
     }