commit - db9db34540318a2a30adac65e9f5eda9bcbb8f26
commit + ee85b57d8b907737e39eaaeafc729611263cf224
blob - 60daa555e4cde3bd257306066ce13139e6ad1df8
blob + 763667227da5f28d4783904e5cb92aa0037a403e
--- Cargo.toml
+++ Cargo.toml
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
use clap::Parser;
-use log::*;
+use log::debug;
use std::path::PathBuf;
#[derive(Parser)]
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 {
}
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
}
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)
}
}
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,
// 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
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
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
.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
///
/// 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());
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,
}
}
+/// 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
+#![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]
.context(format!("File {:?} does not exist", &config.urls_path))?,
)
.lines()
- .map(|l| l.unwrap())
+ .filter_map(Result::ok)
.filter(|l| !l.starts_with('#'))
.collect();
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!(
let mailer = get_mailer(
config.smtp_user.clone(),
config.smtp_password.clone(),
- config.smtp_server.clone(),
+ &config.smtp_server,
config.smtp_port,
)?;
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 {:?}",
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)