From 3d4cf74b47b1e4a32615a2759f3d3c98887f6700 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Thu, 1 Aug 2024 19:48:59 +0100 Subject: [PATCH] WIP: send email notifications --- Cargo.toml | 7 +- crates/cli/Cargo.toml | 4 + .../handlers/email/body-ci-check-failed.txt | 1 + .../src/server/actor/handlers/notify_user.rs | 133 +++++++++++++++++- crates/core/src/config/server.rs | 67 +++++++++ 5 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 crates/cli/src/server/actor/handlers/email/body-ci-check-failed.txt diff --git a/Cargo.toml b/Cargo.toml index 2a77ae9..485f3b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,6 @@ nursery = { level = "warn", priority = -1 } unwrap_used = "warn" expect_used = "warn" -[workspace.lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } - [workspace.dependencies] git-next-core = { path = "crates/core", version = "0.12" } git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.12" } @@ -95,6 +92,10 @@ actix = "0.13" actix-rt = "2.9" tokio = { version = "1.37", features = ["rt", "macros"] } +# email +lettre = "0.11" +sendmail = "2.0" + # Testing assert2 = "0.3" pretty_assertions = "1.4" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e1a65e8..a8b4934 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -66,6 +66,10 @@ warp = { workspace = true } # file watcher (linux) notify = { workspace = true } +# email +lettre = { workspace = true } +sendmail = { workspace = true } + [dev-dependencies] # Testing assert2 = { workspace = true } diff --git a/crates/cli/src/server/actor/handlers/email/body-ci-check-failed.txt b/crates/cli/src/server/actor/handlers/email/body-ci-check-failed.txt new file mode 100644 index 0000000..e933836 --- /dev/null +++ b/crates/cli/src/server/actor/handlers/email/body-ci-check-failed.txt @@ -0,0 +1 @@ +{forge-alias}/{repo-alias}: commit: {commit} diff --git a/crates/cli/src/server/actor/handlers/notify_user.rs b/crates/cli/src/server/actor/handlers/notify_user.rs index 76f4aa3..f41546b 100644 --- a/crates/cli/src/server/actor/handlers/notify_user.rs +++ b/crates/cli/src/server/actor/handlers/notify_user.rs @@ -5,9 +5,14 @@ use secrecy::ExposeSecret; use standardwebhooks::Webhook; use tracing::Instrument; +use std::ops::Deref; + use crate::{repo::messages::NotifyUser, server::actor::ServerActor}; -use git_next_core::server::{self, OutboundWebhook}; +use git_next_core::{ + git::UserNotification, + server::{self, EmailConfig, OutboundWebhook}, +}; impl Handler for ServerActor { type Result = (); @@ -20,7 +25,10 @@ impl Handler for ServerActor { let net = self.net.clone(); async move { if let Some(webhook_config) = shout_config.webhook() { - send_webhook(msg, webhook_config, net).await; + send_webhook(&msg, webhook_config, &net).await; + } + if let Some(email_config) = shout_config.email() { + send_email(&msg, email_config).await; } } .in_current_span() @@ -30,9 +38,9 @@ impl Handler for ServerActor { } async fn send_webhook( - msg: NotifyUser, + msg: &NotifyUser, webhook_config: &OutboundWebhook, - net: kxio::network::Network, + net: &kxio::network::Network, ) { let Ok(webhook) = Webhook::from_bytes(webhook_config.secret().expose_secret().as_bytes().into()) @@ -44,10 +52,10 @@ async fn send_webhook( } async fn do_send_webhook( - msg: NotifyUser, + msg: &NotifyUser, webhook: Webhook, webhook_config: &server::OutboundWebhook, - net: kxio::network::Network, + net: &kxio::network::Network, ) { let message_id = format!("msg_{}", ulid::Ulid::new()); let timestamp = time::OffsetDateTime::now_utc(); @@ -77,3 +85,116 @@ async fn do_send_webhook( |_| (), ); } + +struct EmailMessage { + from: String, + to: String, + subject: String, + body: String, +} + +async fn send_email(msg: &NotifyUser, email_config: &EmailConfig) { + let email_message = EmailMessage { + from: email_config.from().to_string(), + to: email_config.to().to_string(), + subject: email_subject(msg), + body: email_body(msg), + }; + match email_config.smtp() { + Some(smtp) => send_email_smtp(email_message, smtp), + None => send_email_sendmail(email_message), + } +} + +fn email_body(msg: &NotifyUser) -> String { + match msg.deref() { + UserNotification::CICheckFailed { + forge_alias, + repo_alias, + commit, + } => format!("CI Checks had Failed in {forge_alias}/{repo_alias} for the {commit} commit."), + UserNotification::RepoConfigLoadFailure { + forge_alias, + repo_alias, + reason, + } => format!( + "Failed to read/parse the .git-next.toml file for {forge_alias}/{repo_alias}: {reason}" + ), + UserNotification::WebhookRegistration { + forge_alias, + repo_alias, + reason, + } => format!("Failed to register the webhook for {forge_alias}/{repo_alias} with the forge: {reason}"), + UserNotification::DevNotBasedOnMain { + forge_alias, + repo_alias, + dev_branch, + main_branch, + } => format!("In {forge_alias}/{repo_alias}, the dev branch '{dev_branch}' is not based on the main branch '{main_branch}, it needs to be rebased."), + } +} + +fn email_subject(msg: &NotifyUser) -> String { + match msg.deref() { + UserNotification::CICheckFailed { + forge_alias, + repo_alias, + commit, + } => format!("CI Check Failed: {forge_alias}/{repo_alias}: {commit}"), + UserNotification::RepoConfigLoadFailure { + forge_alias, + repo_alias, + reason: _, + } => format!("Invalid Repo Config: {forge_alias}/{repo_alias}"), + UserNotification::WebhookRegistration { + forge_alias, + repo_alias, + reason: _, + } => format!("Failed Webhook Registration: {forge_alias}/{repo_alias}"), + UserNotification::DevNotBasedOnMain { + forge_alias, + repo_alias, + dev_branch: _, + main_branch: _, + } => format!("Dev Not Based on Main: {forge_alias}/{repo_alias}"), + } +} + +fn send_email_sendmail(email_message: EmailMessage) { + use sendmail::email; + match email::send( + &email_message.from, + [email_message.to.as_ref()], + &email_message.subject, + &email_message.body, + ) { + Ok(_) => tracing::info!("Email sent successfully!"), + Err(e) => tracing::warn!("Could not send email: {:?}", e), + } +} + +fn send_email_smtp(email_message: EmailMessage, smtp: &server::SmtpConfig) { + if let Err(err) = do_send_email_smtp(email_message, smtp) { + tracing::warn!(?err, "sending email"); + } +} +fn do_send_email_smtp( + email_message: EmailMessage, + smtp: &server::SmtpConfig, +) -> Result<(), anyhow::Error> { + use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; + + let email = Message::builder() + .from(email_message.from.parse()?) + .to(email_message.to.parse()?) + .subject(email_message.subject) + .body(email_message.body)?; + + let creds = Credentials::new(smtp.username().to_string(), smtp.password().to_string()); + let mailer = SmtpTransport::relay(smtp.hostname())? + .credentials(creds) + .build(); + Ok(mailer.send(&email).map(|response| { + tracing::info!(?response, "email sent via smtp"); + })?) +} diff --git a/crates/core/src/config/server.rs b/crates/core/src/config/server.rs index 4486612..1133862 100644 --- a/crates/core/src/config/server.rs +++ b/crates/core/src/config/server.rs @@ -207,11 +207,13 @@ impl ServerStorage { )] pub struct Shout { webhook: Option, + email: Option, } impl Shout { pub const fn new_webhook(webhook: OutboundWebhook) -> Self { Self { webhook: Some(webhook), + email: None, } } @@ -226,6 +228,10 @@ impl Shout { pub fn webhook_secret(&self) -> Option> { self.webhook.clone().map(|x| x.secret).map(Secret::new) } + + pub const fn email(&self) -> Option<&EmailConfig> { + self.email.as_ref() + } } #[derive( @@ -251,3 +257,64 @@ impl OutboundWebhook { Secret::new(self.secret.clone()) } } + +#[derive( + Clone, + Debug, + derive_more::From, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Deserialize, + derive_more::Constructor, +)] +pub struct EmailConfig { + from: String, + to: String, + // email will be sent via sendmail, unless smtp is specified + smtp: Option, +} +impl EmailConfig { + pub fn from(&self) -> &str { + &self.from + } + + pub fn to(&self) -> &str { + &self.to + } + + pub const fn smtp(&self) -> Option<&SmtpConfig> { + self.smtp.as_ref() + } +} + +#[derive( + Clone, + Debug, + derive_more::From, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Deserialize, + derive_more::Constructor, +)] +pub struct SmtpConfig { + hostname: String, + username: String, + password: String, +} +impl SmtpConfig { + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> &str { + &self.password + } + + pub fn hostname(&self) -> &str { + &self.hostname + } +}