commit - 53515ec93f87219808a6d2bcbe9416f30d235cb0
commit + 55c6cbf79df0b49e557cab18d15ebdc952243d7d
blob - 7044fa665cbe06e9d22808d70360750fb0cb0bb3
blob + c7387c964ec3e2b3c4f9e2e82c5e4e0a3ed9e432
--- docs/rss-email.1.scd
+++ docs/rss-email.1.scd
# SYNOPSIS
-*rss-email* [--config <config-path>] [--database <database-path>] [--dry-run] [-h|--help] [--urls <urls-path>] [-V|--version]
+*rss-email* [OPTIONS]
# DESCRIPTION
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
/// 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
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
-use crate::config::Config;
use lettre::{
message::Message,
transport::smtp::{authentication::Credentials, AsyncSmtpTransport},
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)?;
}
}
-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
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,
#[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)
.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 {
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<()>
{
if !dry_run {
Mail::new(post.title, post.content, post.url)
- .send_email(&config, &mailer)
+ .send_email(from, to, &mailer)
.await?;
}