From 143b2361f6a160639e4d7e57ee922ba7880f0f0d Mon Sep 17 00:00:00 2001 From: Hein Date: Sun, 23 Aug 2026 11:53:07 +0200 Subject: [PATCH] feat(mail): add site URL handling and notification headers * include site URL in Mailer for absolute URLs * add header method for email notifications --- server/internal/mail/mail.go | 32 +++++++++++++++++---- server/main.go | 2 +- ui/src/routes/details/+page.svelte | 45 +++++++++++++++++------------- ui/src/routes/venue/+page.svelte | 31 ++++++++++++-------- 4 files changed, 72 insertions(+), 38 deletions(-) diff --git a/server/internal/mail/mail.go b/server/internal/mail/mail.go index eee7d6c..a12e6c7 100644 --- a/server/internal/mail/mail.go +++ b/server/internal/mail/mail.go @@ -14,11 +14,31 @@ import ( ) type Mailer struct { - cfg config.EmailConfig + cfg config.EmailConfig + siteURL string } -func New(cfg config.EmailConfig) *Mailer { - return &Mailer{cfg: cfg} +func New(cfg config.EmailConfig, siteURL string) *Mailer { + return &Mailer{cfg: cfg, siteURL: strings.TrimSuffix(siteURL, "/")} +} + +// absoluteURL turns a site-relative path (e.g. "/uploads/1/foo.jpg") into a +// full URL so it's clickable straight from an email client. +func (m *Mailer) absoluteURL(path string) string { + if m.siteURL == "" { + return path + } + return m.siteURL + path +} + +// header prefixes every notification with a one-line explainer of where it +// came from and what triggered it, so it's never a mystery in an inbox. +func (m *Mailer) header(event string) string { + site := m.siteURL + if site == "" { + site = "the wedding site" + } + return fmt.Sprintf("This is an automated email that gets triggered for %s when %s.\n\n", site, event) } // newMessageID builds an RFC 5322 Message-ID, scoped to the sender's own @@ -93,7 +113,7 @@ func (m *Mailer) SendRSVP(r store.RSVP, allGuests []store.Guest) error { fmt.Fprintf(&b, "\n%d adult(s), %d child(ren) total\n", totalAdults, totalChildren) } - return m.send(fmt.Sprintf("RSVP from %s", joined), b.String()) + return m.send(fmt.Sprintf("RSVP from %s", joined), m.header("an RSVP is made")+b.String()) } func (m *Mailer) SendPhotoUpload(u store.PhotoUpload, files []store.PhotoFile) error { @@ -110,7 +130,7 @@ func (m *Mailer) SendPhotoUpload(u store.PhotoUpload, files []store.PhotoFile) e } b.WriteString("\nPhotos:\n") for _, f := range files { - fmt.Fprintf(&b, "- %s\n", f.URL) + fmt.Fprintf(&b, "- %s\n", m.absoluteURL(f.URL)) } - return m.send(fmt.Sprintf("%s uploaded photos", u.Name), b.String()) + return m.send(fmt.Sprintf("%s uploaded photos", u.Name), m.header("photos are uploaded")+b.String()) } diff --git a/server/main.go b/server/main.go index 779ca0b..6ef0d1d 100644 --- a/server/main.go +++ b/server/main.go @@ -45,7 +45,7 @@ func main() { } defer st.Close() - mailer := mail.New(cfg.Email) + mailer := mail.New(cfg.Email, cfg.Site.URL) wh := webhook.New(cfg.Webhook) if cfg.Origin.ServiceKey == "" { log.Printf("origin registration disabled: origin.service_key is not configured") diff --git a/ui/src/routes/details/+page.svelte b/ui/src/routes/details/+page.svelte index b86610a..1c9701f 100644 --- a/ui/src/routes/details/+page.svelte +++ b/ui/src/routes/details/+page.svelte @@ -1,4 +1,5 @@ -
-
-

Important Information

-

- Please RSVP by {formatFullDate(wedding.rsvpByDate)}. +

+ + +
+

Important Information

+

+ Please RSVP by {formatFullDate(wedding.rsvpByDate)}.

-

Dress Code: Formal

-

+

Dress Code: Formal

+

We ask that our guests dress in formal attire. Think elegant dresses and sharp suits.

-