feat: notification emails about new or deleted assignments
This commit is contained in:
parent
af7f8a8cc6
commit
6833887254
@ -11,6 +11,8 @@ use lettre::{
|
|||||||
use crate::utils::ApplicationError;
|
use crate::utils::ApplicationError;
|
||||||
|
|
||||||
mod forgot_password;
|
mod forgot_password;
|
||||||
|
mod notify_planned;
|
||||||
|
mod notify_unplanned;
|
||||||
mod registration;
|
mod registration;
|
||||||
mod testmail;
|
mod testmail;
|
||||||
|
|
||||||
|
149
web/src/mail/notify_planned.rs
Normal file
149
web/src/mail/notify_planned.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use brass_db::models::{Assignment, Event, Function, User};
|
||||||
|
use lettre::{
|
||||||
|
message::{Mailbox, MultiPart, SinglePart},
|
||||||
|
Address, AsyncTransport, Message,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
filters,
|
||||||
|
mail::Mailer,
|
||||||
|
utils::{
|
||||||
|
ApplicationError,
|
||||||
|
DateTimeFormat::{DayMonthYear, WeekdayDayMonthYear},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Mailer {
|
||||||
|
pub async fn send_assignment_created_mail(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
assignment: &Assignment,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<(), ApplicationError> {
|
||||||
|
let message = build(
|
||||||
|
&self.hostname,
|
||||||
|
&user.name,
|
||||||
|
&user.email,
|
||||||
|
&assignment.function,
|
||||||
|
event,
|
||||||
|
)?;
|
||||||
|
self.transport.send(message).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "emails/notify_planned.txt")]
|
||||||
|
struct NotifyPlannedMailTemplatePlain<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
function: &'a Function,
|
||||||
|
event: &'a Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "emails/notify_planned.html")]
|
||||||
|
struct NotifyPlannedMailTemplateHtml<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
function: &'a Function,
|
||||||
|
event: &'a Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(
|
||||||
|
hostname: &str,
|
||||||
|
name: &str,
|
||||||
|
email: &str,
|
||||||
|
function: &Function,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<Message, ApplicationError> {
|
||||||
|
let plain = NotifyPlannedMailTemplatePlain {
|
||||||
|
name,
|
||||||
|
function,
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let html = NotifyPlannedMailTemplateHtml {
|
||||||
|
name,
|
||||||
|
function,
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let subject = format!(
|
||||||
|
"Brass: Einteilung bei {} am {} als {function}",
|
||||||
|
event.name,
|
||||||
|
event.start.format(DayMonthYear.into())
|
||||||
|
);
|
||||||
|
|
||||||
|
let sender_mailbox = Mailbox::new(
|
||||||
|
Some("noreply".to_string()),
|
||||||
|
Address::new("noreply", hostname)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Message::builder()
|
||||||
|
.from(sender_mailbox.clone())
|
||||||
|
.reply_to(sender_mailbox)
|
||||||
|
.to(Mailbox::new(Some(name.to_string()), email.parse()?))
|
||||||
|
.subject(subject)
|
||||||
|
.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(SinglePart::plain(plain))
|
||||||
|
.singlepart(SinglePart::html(html)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::utils::test_helper::{assert_mail_snapshot, NaiveDateTimeExt};
|
||||||
|
use brass_db::models::{Clothing, Location};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use lettre::{transport::stub::StubTransport, Transport};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_mail_snapshot() {
|
||||||
|
let event = Event {
|
||||||
|
id: 1,
|
||||||
|
start: NaiveDateTime::from_ymd_and_hms(2025, 06, 01, 18, 30, 0).unwrap(),
|
||||||
|
end: NaiveDateTime::from_ymd_and_hms(2025, 06, 02, 4, 0, 0).unwrap(),
|
||||||
|
name: "Wave Gotik Treffen".to_string(),
|
||||||
|
location_id: 1,
|
||||||
|
location: Some(Location {
|
||||||
|
id: 1,
|
||||||
|
name: "agra Messe Leipzig".to_string(),
|
||||||
|
area_id: 1,
|
||||||
|
area: None,
|
||||||
|
}),
|
||||||
|
voluntary_wachhabender: false,
|
||||||
|
fuehrungsassistent_required: false,
|
||||||
|
amount_of_posten: 3,
|
||||||
|
clothing: Clothing {
|
||||||
|
id: 1,
|
||||||
|
name: "komplette PSA".to_string(),
|
||||||
|
},
|
||||||
|
canceled: false,
|
||||||
|
note: Some("Anfahrt Bereitstellungsraum über xyz...".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = build(
|
||||||
|
"brasiwa-leipzig.de",
|
||||||
|
"Max Mustermann",
|
||||||
|
"max@example.com",
|
||||||
|
&Function::Posten,
|
||||||
|
&event,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let sender = StubTransport::new_ok();
|
||||||
|
sender.send(&message).unwrap();
|
||||||
|
let messages = sender.messages();
|
||||||
|
let (_, sent_message) = messages.first().unwrap();
|
||||||
|
|
||||||
|
assert_mail_snapshot!(sent_message);
|
||||||
|
}
|
||||||
|
}
|
149
web/src/mail/notify_unplanned.rs
Normal file
149
web/src/mail/notify_unplanned.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use brass_db::models::{Assignment, Event, Function, User};
|
||||||
|
use lettre::{
|
||||||
|
message::{Mailbox, MultiPart, SinglePart},
|
||||||
|
Address, AsyncTransport, Message,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
filters,
|
||||||
|
mail::Mailer,
|
||||||
|
utils::{
|
||||||
|
ApplicationError,
|
||||||
|
DateTimeFormat::{DayMonthYear, WeekdayDayMonthYear},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Mailer {
|
||||||
|
pub async fn send_assignment_deleted_mail(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
assignment: &Assignment,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<(), ApplicationError> {
|
||||||
|
let message = build(
|
||||||
|
&self.hostname,
|
||||||
|
&user.name,
|
||||||
|
&user.email,
|
||||||
|
&assignment.function,
|
||||||
|
event,
|
||||||
|
)?;
|
||||||
|
self.transport.send(message).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "emails/notify_unplanned.txt")]
|
||||||
|
struct NotifyUnplannedMailTemplatePlain<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
function: &'a Function,
|
||||||
|
event: &'a Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "emails/notify_unplanned.html")]
|
||||||
|
struct NotifyUnplannedMailTemplateHtml<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
function: &'a Function,
|
||||||
|
event: &'a Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(
|
||||||
|
hostname: &str,
|
||||||
|
name: &str,
|
||||||
|
email: &str,
|
||||||
|
function: &Function,
|
||||||
|
event: &Event,
|
||||||
|
) -> Result<Message, ApplicationError> {
|
||||||
|
let plain = NotifyUnplannedMailTemplatePlain {
|
||||||
|
name,
|
||||||
|
function,
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let html = NotifyUnplannedMailTemplateHtml {
|
||||||
|
name,
|
||||||
|
function,
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let subject = format!(
|
||||||
|
"Brass: Rücknahme der Einteilung bei {} am {} als {function}",
|
||||||
|
event.name,
|
||||||
|
event.start.format(DayMonthYear.into())
|
||||||
|
);
|
||||||
|
|
||||||
|
let sender_mailbox = Mailbox::new(
|
||||||
|
Some("noreply".to_string()),
|
||||||
|
Address::new("noreply", hostname)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Message::builder()
|
||||||
|
.from(sender_mailbox.clone())
|
||||||
|
.reply_to(sender_mailbox)
|
||||||
|
.to(Mailbox::new(Some(name.to_string()), email.parse()?))
|
||||||
|
.subject(subject)
|
||||||
|
.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(SinglePart::plain(plain))
|
||||||
|
.singlepart(SinglePart::html(html)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::utils::test_helper::{assert_mail_snapshot, NaiveDateTimeExt};
|
||||||
|
use brass_db::models::{Clothing, Location};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use lettre::{transport::stub::StubTransport, Transport};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_mail_snapshot() {
|
||||||
|
let event = Event {
|
||||||
|
id: 1,
|
||||||
|
start: NaiveDateTime::from_ymd_and_hms(2025, 06, 01, 18, 30, 0).unwrap(),
|
||||||
|
end: NaiveDateTime::from_ymd_and_hms(2025, 06, 02, 4, 0, 0).unwrap(),
|
||||||
|
name: "Wave Gotik Treffen".to_string(),
|
||||||
|
location_id: 1,
|
||||||
|
location: Some(Location {
|
||||||
|
id: 1,
|
||||||
|
name: "agra Messe Leipzig".to_string(),
|
||||||
|
area_id: 1,
|
||||||
|
area: None,
|
||||||
|
}),
|
||||||
|
voluntary_wachhabender: false,
|
||||||
|
fuehrungsassistent_required: false,
|
||||||
|
amount_of_posten: 3,
|
||||||
|
clothing: Clothing {
|
||||||
|
id: 1,
|
||||||
|
name: "komplette PSA".to_string(),
|
||||||
|
},
|
||||||
|
canceled: false,
|
||||||
|
note: Some("Anfahrt Bereitstellungsraum über xyz...".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = build(
|
||||||
|
"brasiwa-leipzig.de",
|
||||||
|
"Max Mustermann",
|
||||||
|
"max@example.com",
|
||||||
|
&Function::Posten,
|
||||||
|
&event,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let sender = StubTransport::new_ok();
|
||||||
|
sender.send(&message).unwrap();
|
||||||
|
let messages = sender.messages();
|
||||||
|
let (_, sent_message) = messages.first().unwrap();
|
||||||
|
|
||||||
|
assert_mail_snapshot!(sent_message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
source: web/src/mail/notify_planned.rs
|
||||||
|
expression: sent_message
|
||||||
|
snapshot_kind: text
|
||||||
|
---
|
||||||
|
From: noreply <noreply@brasiwa-leipzig.de>
|
||||||
|
Reply-To: noreply <noreply@brasiwa-leipzig.de>
|
||||||
|
To: "Max Mustermann" <max@example.com>
|
||||||
|
Subject: Brass: Einteilung bei Wave Gotik Treffen am 01.06.2025 als Posten
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Date
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="boundary"
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hallo Max Mustermann,
|
||||||
|
|
||||||
|
du wurdest soeben f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonntag,=
|
||||||
|
01.06.2025 als Posten eingeteilt.
|
||||||
|
|
||||||
|
Hier die Infos zur Veranstaltung:
|
||||||
|
- Name: Wave Gotik Treffen
|
||||||
|
- Ort: agra Messe Leipzig
|
||||||
|
- Beginn: 2025-06-01 18:30:00
|
||||||
|
- Ende: 2025-06-02 04:00:00
|
||||||
|
- Kleidungsordnung: komplette PSA
|
||||||
|
- Anmerkungen: Anfahrt Bereitstellungsraum =C3=BCber xyz...
|
||||||
|
|
||||||
|
Falls dir etwas dazwischen kommt, melde dich bei deinem Bereichsleiter.
|
||||||
|
|
||||||
|
Viele Gr=C3=BC=C3=9Fe
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<p>Hallo Max Mustermann,</p>
|
||||||
|
|
||||||
|
<p>du wurdest soeben f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonnt=
|
||||||
|
ag, 01.06.2025 als Posten eingeteilt.</p>
|
||||||
|
|
||||||
|
<p>Hier die Infos zur Veranstaltung:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Name: Wave Gotik Treffen</li>
|
||||||
|
<li>- Ort: agra Messe Leipzig</li>
|
||||||
|
<li>Beginn: 2025-06-01 18:30:00</li>
|
||||||
|
<li>Ende: 2025-06-02 04:00:00</li>
|
||||||
|
<li>Kleidungsordnung: komplette PSA</li>
|
||||||
|
<li>Anmerkungen: Anfahrt Bereitstellungsraum =C3=BCber xyz...</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Falls dir etwas dazwischen kommt, melde dich bei deinem Bereichsleiter.<=
|
||||||
|
/p>
|
||||||
|
|
||||||
|
<p>Viele Gr=C3=BC=C3=9Fe</p>
|
||||||
|
--boundary--
|
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
source: web/src/mail/notify_unplanned.rs
|
||||||
|
expression: sent_message
|
||||||
|
snapshot_kind: text
|
||||||
|
---
|
||||||
|
From: noreply <noreply@brasiwa-leipzig.de>
|
||||||
|
Reply-To: noreply <noreply@brasiwa-leipzig.de>
|
||||||
|
To: "Max Mustermann" <max@example.com>
|
||||||
|
Subject: Brass: =?utf-8?b?UsO8Y2tuYWhtZQ==?= der Einteilung bei Wave Gotik
|
||||||
|
Treffen am 01.06.2025 als Posten
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Date
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="boundary"
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hallo Max Mustermann,
|
||||||
|
|
||||||
|
deine Einteilung f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonntag, =
|
||||||
|
01.06.2025 als Posten wurde zur=C3=BCckgenommen.
|
||||||
|
|
||||||
|
Bei Fragen melde dich bei deinem Bereichsleiter.
|
||||||
|
|
||||||
|
Viele Gr=C3=BC=C3=9Fe
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<p>Hallo Max Mustermann,</p>
|
||||||
|
|
||||||
|
<p>deine Einteilung f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonnta=
|
||||||
|
g, 01.06.2025 als Posten wurde zur=C3=BCckgenommen.</p>
|
||||||
|
|
||||||
|
<p>Bei Fragen melde dich bei deinem Bereichsleiter.</p>
|
||||||
|
|
||||||
|
<p>Viele Gr=C3=BC=C3=9Fe</p>
|
||||||
|
--boundary--
|
18
web/templates/emails/notify_planned.html
Normal file
18
web/templates/emails/notify_planned.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<p>Hallo {{ name }},</p>
|
||||||
|
|
||||||
|
<p>du wurdest soeben für die Veranstaltung {{ event.name }} am {{ event.start|fmt_datetime(WeekdayDayMonthYear) }} als {{ function }} eingeteilt.</p>
|
||||||
|
|
||||||
|
<p>Hier die Infos zur Veranstaltung:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Name: {{ event.name }}</li>
|
||||||
|
<li>- Ort: {{ event.location.as_ref().unwrap().name }}</li>
|
||||||
|
<li>Beginn: {{ event.start }}</li>
|
||||||
|
<li>Ende: {{ event.end }}</li>
|
||||||
|
<li>Kleidungsordnung: {{ event.clothing.name }}</li>
|
||||||
|
{% if let Some(note) = event.note %}<li>Anmerkungen: {{ note }}</li>{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Falls dir etwas dazwischen kommt, melde dich bei deinem Bereichsleiter.</p>
|
||||||
|
|
||||||
|
<p>Viele Grüße</p>
|
15
web/templates/emails/notify_planned.txt
Normal file
15
web/templates/emails/notify_planned.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
Hallo {{ name }},
|
||||||
|
|
||||||
|
du wurdest soeben für die Veranstaltung {{ event.name }} am {{ event.start|fmt_datetime(WeekdayDayMonthYear) }} als {{ function }} eingeteilt.
|
||||||
|
|
||||||
|
Hier die Infos zur Veranstaltung:
|
||||||
|
- Name: {{ event.name }}
|
||||||
|
- Ort: {{ event.location.as_ref().unwrap().name }}
|
||||||
|
- Beginn: {{ event.start }}
|
||||||
|
- Ende: {{ event.end }}
|
||||||
|
- Kleidungsordnung: {{ event.clothing.name }}
|
||||||
|
{% if let Some(note) = event.note %}- Anmerkungen: {{ note }}{% endif %}
|
||||||
|
|
||||||
|
Falls dir etwas dazwischen kommt, melde dich bei deinem Bereichsleiter.
|
||||||
|
|
||||||
|
Viele Grüße
|
7
web/templates/emails/notify_unplanned.html
Normal file
7
web/templates/emails/notify_unplanned.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<p>Hallo {{ name }},</p>
|
||||||
|
|
||||||
|
<p>deine Einteilung für die Veranstaltung {{ event.name }} am {{ event.start|fmt_datetime(WeekdayDayMonthYear) }} als {{ function }} wurde zurückgenommen.</p>
|
||||||
|
|
||||||
|
<p>Bei Fragen melde dich bei deinem Bereichsleiter.</p>
|
||||||
|
|
||||||
|
<p>Viele Grüße</p>
|
7
web/templates/emails/notify_unplanned.txt
Normal file
7
web/templates/emails/notify_unplanned.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Hallo {{ name }},
|
||||||
|
|
||||||
|
deine Einteilung für die Veranstaltung {{ event.name }} am {{ event.start|fmt_datetime(WeekdayDayMonthYear) }} als {{ function }} wurde zurückgenommen.
|
||||||
|
|
||||||
|
Bei Fragen melde dich bei deinem Bereichsleiter.
|
||||||
|
|
||||||
|
Viele Grüße
|
Loading…
x
Reference in New Issue
Block a user