diff --git a/web/src/mail/mod.rs b/web/src/mail/mod.rs index 84fefff0..e9c9df70 100644 --- a/web/src/mail/mod.rs +++ b/web/src/mail/mod.rs @@ -11,6 +11,8 @@ use lettre::{ use crate::utils::ApplicationError; mod forgot_password; +mod notify_planned; +mod notify_unplanned; mod registration; mod testmail; diff --git a/web/src/mail/notify_planned.rs b/web/src/mail/notify_planned.rs new file mode 100644 index 00000000..baa78005 --- /dev/null +++ b/web/src/mail/notify_planned.rs @@ -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 { + 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); + } +} diff --git a/web/src/mail/notify_unplanned.rs b/web/src/mail/notify_unplanned.rs new file mode 100644 index 00000000..d2e66f6d --- /dev/null +++ b/web/src/mail/notify_unplanned.rs @@ -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 { + 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); + } +} diff --git a/web/src/mail/snapshots/brass_web__mail__notify_planned__tests__build_mail_snapshot.snap b/web/src/mail/snapshots/brass_web__mail__notify_planned__tests__build_mail_snapshot.snap new file mode 100644 index 00000000..23107874 --- /dev/null +++ b/web/src/mail/snapshots/brass_web__mail__notify_planned__tests__build_mail_snapshot.snap @@ -0,0 +1,59 @@ +--- +source: web/src/mail/notify_planned.rs +expression: sent_message +snapshot_kind: text +--- +From: noreply +Reply-To: noreply +To: "Max Mustermann" +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 + +

Hallo Max Mustermann,

+ +

du wurdest soeben f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonnt= +ag, 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.<= +/p> + +

Viele Gr=C3=BC=C3=9Fe

+--boundary-- diff --git a/web/src/mail/snapshots/brass_web__mail__notify_unplanned__tests__build_mail_snapshot.snap b/web/src/mail/snapshots/brass_web__mail__notify_unplanned__tests__build_mail_snapshot.snap new file mode 100644 index 00000000..80ea9216 --- /dev/null +++ b/web/src/mail/snapshots/brass_web__mail__notify_unplanned__tests__build_mail_snapshot.snap @@ -0,0 +1,40 @@ +--- +source: web/src/mail/notify_unplanned.rs +expression: sent_message +snapshot_kind: text +--- +From: noreply +Reply-To: noreply +To: "Max Mustermann" +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 + +

Hallo Max Mustermann,

+ +

deine Einteilung f=C3=BCr die Veranstaltung Wave Gotik Treffen am Sonnta= +g, 01.06.2025 als Posten wurde zur=C3=BCckgenommen.

+ +

Bei Fragen melde dich bei deinem Bereichsleiter.

+ +

Viele Gr=C3=BC=C3=9Fe

+--boundary-- diff --git a/web/templates/emails/notify_planned.html b/web/templates/emails/notify_planned.html new file mode 100644 index 00000000..3742becc --- /dev/null +++ b/web/templates/emails/notify_planned.html @@ -0,0 +1,18 @@ +

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

diff --git a/web/templates/emails/notify_planned.txt b/web/templates/emails/notify_planned.txt new file mode 100644 index 00000000..3d873f2e --- /dev/null +++ b/web/templates/emails/notify_planned.txt @@ -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 diff --git a/web/templates/emails/notify_unplanned.html b/web/templates/emails/notify_unplanned.html new file mode 100644 index 00000000..e3c661d6 --- /dev/null +++ b/web/templates/emails/notify_unplanned.html @@ -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

diff --git a/web/templates/emails/notify_unplanned.txt b/web/templates/emails/notify_unplanned.txt new file mode 100644 index 00000000..1ce93f6e --- /dev/null +++ b/web/templates/emails/notify_unplanned.txt @@ -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