WIP: send email notifications
This commit is contained in:
parent
538728c491
commit
eb6a7f9933
6 changed files with 241 additions and 36 deletions
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
123
crates/cli/src/server/actor/handlers/notify_user/email.rs
Normal file
123
crates/cli/src/server/actor/handlers/notify_user/email.rs
Normal 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}"),
|
||||
}
|
||||
}
|
34
crates/cli/src/server/actor/handlers/notify_user/mod.rs
Normal file
34
crates/cli/src/server/actor/handlers/notify_user/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue