commit ee85b57d8b907737e39eaaeafc729611263cf224 from: witcher date: Thu Jan 5 16:09:12 2023 UTC Add more clippy lints and make clippy happy :) commit - db9db34540318a2a30adac65e9f5eda9bcbb8f26 commit + ee85b57d8b907737e39eaaeafc729611263cf224 blob - 60daa555e4cde3bd257306066ce13139e6ad1df8 blob + 763667227da5f28d4783904e5cb92aa0037a403e --- Cargo.toml +++ Cargo.toml @@ -5,7 +5,9 @@ authors = ["witcher "] 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 { 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) -> anyhow::Result { + pub fn new(config_path: impl AsRef) -> anyhow::Result { 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 { 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, 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>(url: S) -> anyhow::Result> { +/// 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(url: S) -> anyhow::Result> +where + S: AsRef + 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> { +/// 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> { 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::>()) } -pub async fn fetch_new_atom(bytes: &[u8]) -> anyhow::Result> { +/// 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> { 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, body: Option, url: Option) -> 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> { let creds = Credentials::new(user, password); - let mailer = AsyncSmtpTransport::::relay(&server)? + let mailer = AsyncSmtpTransport::::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, 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)