WIP: send email notifications
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful

This commit is contained in:
Paul Campbell 2024-08-01 19:48:59 +01:00 committed by Paul Campbell
parent 538728c491
commit eb6a7f9933
6 changed files with 241 additions and 36 deletions

View file

@ -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"

View file

@ -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 }

View file

@ -0,0 +1,123 @@
use std::ops::Deref as _;
use git_next_core::{
git::UserNotification,
server::{EmailConfig, SmtpConfig},
};
use tracing::instrument;
use crate::repo::messages::NotifyUser;
#[derive(Debug)]
struct EmailMessage {
from: String,
to: String,
subject: String,
body: String,
}
#[instrument]
pub(super) 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),
}
}
#[instrument]
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),
}
}
#[instrument]
fn send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) {
if let Err(err) = do_send_email_smtp(email_message, smtp) {
tracing::warn!(?err, "sending email");
}
}
#[instrument]
fn do_send_email_smtp(email_message: EmailMessage, smtp: &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");
})?)
}
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}"),
}
}

View file

@ -0,0 +1,34 @@
//
mod email;
mod webhook;
use actix::prelude::*;
use email::send_email;
use tracing::Instrument;
use webhook::send_webhook;
use crate::{repo::messages::NotifyUser, server::actor::ServerActor};
impl Handler<NotifyUser> for ServerActor {
type Result = ();
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
let Some(server_config) = &self.server_config else {
return;
};
let shout_config = server_config.shout().clone();
let net = self.net.clone();
async move {
if let Some(webhook_config) = shout_config.webhook() {
send_webhook(&msg, webhook_config, &net).await;
}
if let Some(email_config) = shout_config.email() {
send_email(&msg, email_config);
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
}

View file

@ -1,38 +1,14 @@
//
use actix::prelude::*;
use secrecy::ExposeSecret;
use git_next_core::server::OutboundWebhook;
use secrecy::ExposeSecret as _;
use standardwebhooks::Webhook;
use tracing::Instrument;
use crate::{repo::messages::NotifyUser, server::actor::ServerActor};
use crate::repo::messages::NotifyUser;
use git_next_core::server::{self, OutboundWebhook};
impl Handler<NotifyUser> for ServerActor {
type Result = ();
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
let Some(server_config) = &self.server_config else {
return;
};
let shout_config = server_config.shout().clone();
let net = self.net.clone();
async move {
if let Some(webhook_config) = shout_config.webhook() {
send_webhook(msg, webhook_config, net).await;
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
}
async fn send_webhook(
msg: NotifyUser,
pub(super) async fn send_webhook(
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 +20,10 @@ async fn send_webhook(
}
async fn do_send_webhook(
msg: NotifyUser,
msg: &NotifyUser,
webhook: Webhook,
webhook_config: &server::OutboundWebhook,
net: kxio::network::Network,
webhook_config: &OutboundWebhook,
net: &kxio::network::Network,
) {
let message_id = format!("msg_{}", ulid::Ulid::new());
let timestamp = time::OffsetDateTime::now_utc();

View file

@ -207,11 +207,13 @@ impl ServerStorage {
)]
pub struct Shout {
webhook: Option<OutboundWebhook>,
email: Option<EmailConfig>,
}
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<Secret<String>> {
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<SmtpConfig>,
}
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
}
}