Commit Diff


commit - db9db34540318a2a30adac65e9f5eda9bcbb8f26
commit + ee85b57d8b907737e39eaaeafc729611263cf224
blob - 60daa555e4cde3bd257306066ce13139e6ad1df8
blob + 763667227da5f28d4783904e5cb92aa0037a403e
--- Cargo.toml
+++ Cargo.toml
@@ -5,7 +5,9 @@ authors = ["witcher <witcher@wiredspace.de>"]
 readme = "README.md"
 description = "Fetch RSS Feeds and send them via email"
 homepage = "https://sr.ht/~witcher/rss-email"
+repository = "https://git.sr.ht/~witcher/rss-email"
 keywords = ["email", "rss", "atom"]
+categories = ["command-line-utilities", "email"]
 license = "WTFPL"
 edition = "2021"
 
blob - 15c82241556da71af597256b977c64a5895bb81d
blob + 2066caf7a569852f8bf6e3708166090170fbb650
--- src/cli.rs
+++ src/cli.rs
@@ -1,5 +1,5 @@
 use clap::Parser;
-use log::*;
+use log::debug;
 use std::path::PathBuf;
 
 #[derive(Parser)]
@@ -48,7 +48,7 @@ impl Cli {
     pub fn build_app() -> anyhow::Result<Self> {
         use std::fs::File;
 
-        let args = Cli::parse();
+        let args = Self::parse();
 
         // setup logging as soon as possible
         let verbosity = match args.verbose {
@@ -81,6 +81,7 @@ impl Cli {
 }
 
 fn project_dirs() -> directories::ProjectDirs {
+    #[allow(clippy::unwrap_used)]
     directories::ProjectDirs::from("", "", "rss-email").unwrap()
 }
 
blob - d46d7749a93da15b759f443de302779d14f7635e
blob + 19c0347dd1a3404536a46f3453be28a332f5cc67
--- src/config.rs
+++ src/config.rs
@@ -24,12 +24,12 @@ pub struct SmtpConfig {
 }
 
 impl Config {
-    pub fn new(config_path: impl AsRef<Path>) -> anyhow::Result<Config> {
+    pub fn new(config_path: impl AsRef<Path>) -> anyhow::Result<Self> {
         let mut string = String::new();
         File::open(config_path.as_ref())
             .context(format!("File {:?} does not exist", &config_path.as_ref()))?
             .read_to_string(&mut string)?;
-        let config: Config = toml::de::from_str(&string)?;
+        let config: Self = toml::de::from_str(&string)?;
 
         Ok(config)
     }
@@ -55,9 +55,13 @@ pub struct AppConfig {
 }
 
 impl AppConfig {
+    /// Create a new [`AppConfig`].
+    ///
+    /// Internally, this will parse the command line arguments with [`clap`] and read the
+    /// configuration file *if* all of the required options have *not* been given.
     pub fn new() -> anyhow::Result<Self> {
         let cli = crate::cli::Cli::build_app()?;
-        let mut s = AppConfig {
+        let mut s = Self {
             database_path: cli.database_path,
             urls_path: cli.urls_path,
             dry_run: cli.dry_run,
@@ -67,28 +71,39 @@ impl AppConfig {
 
         // 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();
+        match (
+            cli.mail_from,
+            cli.mail_to,
+            cli.smtp_user,
+            cli.smtp_password,
+            cli.smtp_server,
+            cli.smtp_port,
+        ) {
+            (
+                Some(mail_from),
+                Some(mail_to),
+                Some(smtp_user),
+                Some(smtp_password),
+                Some(smtp_server),
+                Some(smtp_port),
+            ) => {
+                trace!("All necessary config values have been given on the command line, no need to check for config file");
+                s.mail_from = mail_from;
+                s.mail_to = mail_to;
+                s.smtp_user = smtp_user;
+                s.smtp_password = smtp_password;
+                s.smtp_server = smtp_server;
+                s.smtp_port = smtp_port;
+            }
+            (mail_from, mail_to, smtp_user, smtp_password, smtp_server, smtp_port) => {
+                let cfg = Config::new(cli.config_path)?;
+                s.mail_from = mail_from.unwrap_or(cfg.mail.from);
+                s.mail_to = mail_to.unwrap_or(cfg.mail.to);
+                s.smtp_user = smtp_user.unwrap_or(cfg.smtp.user);
+                s.smtp_password = smtp_password.unwrap_or(cfg.smtp.password);
+                s.smtp_server = smtp_server.unwrap_or(cfg.smtp.server);
+                s.smtp_port = smtp_port.unwrap_or(cfg.smtp.port);
+            }
         }
 
         Ok(s)
blob - 9f52e514434f9041afc4835bbec81a22f118280c
blob + c1797f8dd887d33f4370e8cdfef1f3a55b19d7c5
--- src/db.rs
+++ src/db.rs
@@ -3,7 +3,11 @@ use sqlx::Sqlite;
 
 use crate::models::Post;
 
-// inserts a new post or updates an old one with the same guid
+/// Insert a new post or update an old one with the same GUID.
+///
+/// # Errors
+///
+/// An error occurs if the statement cannot be executed.
 pub async fn insert_item(mut conn: PoolConnection<Sqlite>, post: &Post) -> anyhow::Result<()> {
     sqlx::query!(
         "insert or ignore into posts (guid, title, url, pub_date, content) values (?, ?, ?, ?, ?)",
blob - ef181a36c89292c45104d4bf4f07468aadda3466
blob + 66138f66e52c9c8c9b8e92701e1eabb768489ecd
--- src/feed.rs
+++ src/feed.rs
@@ -4,16 +4,30 @@ use rss;
 use crate::anyhow::Context;
 use crate::models::Post;
 
-pub async fn fetch_new<S: AsRef<str>>(url: S) -> anyhow::Result<Vec<Post>> {
+/// Fetch a new feed, RSS or Atom.
+///
+/// # Errors
+///
+/// An error occurs if the feed cannot be fetched or is invalid.
+pub async fn fetch_new<S>(url: S) -> anyhow::Result<Vec<Post>>
+where
+    S: AsRef<str> + Send,
+{
     debug!("Fetching feed for {}", url.as_ref());
-    let content = reqwest::get(url.as_ref()).await?.bytes().await?;
-    match fetch_new_rss(&content[..]).await {
-        Err(_) => fetch_new_atom(&content[..]).await,
+    let url = url.as_ref();
+    let content = reqwest::get(url).await?.bytes().await?;
+    match fetch_new_rss(&content[..]) {
+        Err(_) => fetch_new_atom(&content[..]),
         p => p,
     }
 }
 
-pub async fn fetch_new_rss(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
+/// Build a new rss feed from a given byte slice.
+///
+/// # Errors
+///
+/// An error occurs if the given bytes are an invalid RSS feed.
+pub fn fetch_new_rss(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
     let channel = rss::Channel::read_from(bytes).context("Unable to read from RSS feed")?;
 
     Ok(channel
@@ -29,7 +43,12 @@ pub async fn fetch_new_rss(bytes: &[u8]) -> anyhow::Re
         .collect::<Vec<Post>>())
 }
 
-pub async fn fetch_new_atom(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
+/// Build a new Atom feed from a given byte slice.
+///
+/// # Errors
+///
+/// An error occurs if the given bytes are an invalid Atom feed.
+pub fn fetch_new_atom(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
     let feed = atom_syndication::Feed::read_from(bytes).context("Unable to read from atom feed")?;
 
     Ok(feed
blob - 7d5c380917d5269d733a659e5f8c13492a0b535d
blob + 1f217a66fd933723bd85b74832ff8e3ce3a3d71c
--- src/mail.rs
+++ src/mail.rs
@@ -14,6 +14,7 @@ impl Mail {
     ///
     /// Build a Mail from the optional components subject, body, and url.
     /// If any of the parameters are not given, they will be replaced with a default value.
+    #[must_use]
     pub fn new(subject: Option<String>, body: Option<String>, url: Option<String>) -> Self {
         let subject = subject.unwrap_or_else(|| "No title found".to_string());
         let url = url.unwrap_or_else(|| "No url found".to_string());
@@ -25,6 +26,11 @@ impl Mail {
         Self { subject, body }
     }
 
+    /// Send an email with the body of `self`.
+    ///
+    /// # Errors
+    ///
+    /// An error occurs if the email cannot be built or the email cannot be sent.
     pub async fn send_email<'a>(
         self,
         from: &'a str,
@@ -44,15 +50,22 @@ impl Mail {
     }
 }
 
+/// Get the mailer for sending mail.
+///
+/// # Errors
+///
+/// An error occures if instantiating a [`AsyncSmtpTransport`] in the body fails
+///
+/// [`AsyncSmtpTransport`]: lettre::transport::smtp::AsyncSmtpTransport
 pub fn get_mailer(
     user: String,
     password: String,
-    server: String,
+    server: &str,
     port: u16,
 ) -> anyhow::Result<AsyncSmtpTransport<Tokio1Executor>> {
     let creds = Credentials::new(user, password);
 
-    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
+    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(server)?
         .credentials(creds)
         .port(port)
         .build();
blob - 94fa3d15a1396682a4eee9b2de0172d4d5315d3e
blob + ac60152648ef721ce4e6f3ba1a6413233672ee48
--- src/main.rs
+++ src/main.rs
@@ -1,3 +1,19 @@
+#![deny(
+    clippy::pedantic,
+    clippy::nursery,
+    clippy::unwrap_used,
+    clippy::expect_used,
+    clippy::cargo,
+    clippy::style,
+    clippy::complexity,
+    clippy::correctness,
+    clippy::unseparated_literal_suffix,
+    clippy::try_err,
+    clippy::almost_swapped,
+    clippy::approx_constant
+)]
+#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
+
 #[macro_use]
 extern crate log;
 #[macro_use]
@@ -33,7 +49,7 @@ async fn main() -> anyhow::Result<()> {
             .context(format!("File {:?} does not exist", &config.urls_path))?,
     )
     .lines()
-    .map(|l| l.unwrap())
+    .filter_map(Result::ok)
     .filter(|l| !l.starts_with('#'))
     .collect();
 
@@ -46,10 +62,10 @@ async fn main() -> anyhow::Result<()> {
 
     sqlx::migrate!("./migrations").run(&pool).await?;
 
-    if !config.no_fetch {
-        fetch_feeds(urls, &pool).await?;
-    } else {
+    if config.no_fetch {
         info!("Not fetching any feeds as \"--no-fetch\" has been passed");
+    } else {
+        fetch_feeds(urls, &pool).await?;
     }
 
     let results = sqlx::query_as!(
@@ -62,7 +78,7 @@ async fn main() -> anyhow::Result<()> {
     let mailer = get_mailer(
         config.smtp_user.clone(),
         config.smtp_password.clone(),
-        config.smtp_server.clone(),
+        &config.smtp_server,
         config.smtp_port,
     )?;
 
@@ -104,7 +120,7 @@ async fn fetch_feeds(urls: Vec<String>, pool: &sqlx::P
     while let Some(new) = set.join_next().await {
         let posts = new??;
 
-        for i in posts.into_iter() {
+        for i in posts {
             let conn = pool.acquire().await?;
             db::insert_item(conn, &i).await.context(format!(
                 "Unable to insert item from {:?} with GUID {:?}",
@@ -127,12 +143,12 @@ async fn send_post<'a, E>(
 where
     E: sqlx::Executor<'a, Database = Sqlite>,
 {
-    if !dry_run {
+    if dry_run {
+        info!("Not sending any emails as \"--dry-run\" has been passed");
+    } else {
         Mail::new(post.title, post.content, post.url)
             .send_email(from, to, &mailer)
             .await?;
-    } else {
-        info!("Not sending any emails as \"--dry-run\" has been passed");
     }
 
     sqlx::query!("update posts set sent = true where guid = ?", post.guid)