feat(mail): add site URL handling and notification headers

* include site URL in Mailer for absolute URLs
* add header method for email notifications
This commit is contained in:
2026-08-23 11:53:07 +02:00
parent f96465b798
commit 143b2361f6
4 changed files with 72 additions and 38 deletions
+26 -6
View File
@@ -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())
}
+1 -1
View File
@@ -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")
+26 -19
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PhotoBackground from "$lib/components/PhotoBackground.svelte";
import { createWeddingInfo, formatFullDate } from "$lib/wedding-info.svelte";
import { buildIcsDataUrl } from "$lib/calendar";
@@ -29,33 +30,39 @@
];
</script>
<section class="flex flex-1 items-center justify-center px-4 py-16">
<div class="w-full max-w-md">
<h1 class="font-serif text-4xl">Important Information</h1>
<p class="text-secondary-600-400 mt-2 text-2xl">
Please <a href="/rsvp" class="underline hover:text-secondary-500">RSVP</a> by {formatFullDate(wedding.rsvpByDate)}.
<section
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16 z-10"
>
<PhotoBackground />
<div
class="border-surface-50/70 text-shadow-sm text-shadow-black/60 relative z-10 w-full max-w-md border px-6 py-12 sm:px-14 sm:py-16"
>
<h1 class="text-surface-50 font-serif text-4xl">Important Information</h1>
<p class="text-secondary-300 mt-2 text-2xl">
Please <a href="/rsvp" class="underline hover:text-secondary-400">RSVP</a> by {formatFullDate(wedding.rsvpByDate)}.
</p>
<div class="mt-6 flex flex-col gap-1">
<h2 class="font-serif text-3xl">Dress Code: Formal</h2>
<p class="text-surface-600-400 text-2xl">
<h2 class="text-surface-50 font-serif text-3xl">Dress Code: Formal</h2>
<p class="text-surface-100 text-2xl">
We ask that our guests dress in formal attire. Think elegant dresses
and sharp suits.
</p>
</div>
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-3xl">Food &amp; Drinks</h2>
<p class="text-surface-600-400 mt-2 text-2xl">
<div class="border-surface-50/30 mt-10 border-t pt-6">
<h2 class="text-surface-50 font-serif text-3xl">Food &amp; Drinks</h2>
<p class="text-surface-100 mt-2 text-2xl">
A delicious dinner will be served. Please note that a cash bar is
available for the evening. Card facilities will be available, but we
suggest bringing cash just in case.
</p>
</div>
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-3xl">Honeymoon Fund</h2>
<p class="text-surface-600-400 mt-2 text-2xl">
<div class="border-surface-50/30 mt-10 border-t pt-6">
<h2 class="text-surface-50 font-serif text-3xl">Honeymoon Fund</h2>
<p class="text-surface-100 mt-2 text-2xl">
Your presence on our special day is the greatest gift we could ask
for. However, if you would like to honour us with a gift, a
contribution toward our future home or honeymoon would be warmly
@@ -63,9 +70,9 @@
</p>
</div>
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-3xl">Order of Events</h2>
<p class="text-surface-600-400 mt-2 text-2xl">
<div class="border-surface-50/30 mt-10 border-t pt-6">
<h2 class="text-surface-50 font-serif text-3xl">Order of Events</h2>
<p class="text-surface-100 mt-2 text-2xl">
Zoé &amp; Shaldon — Saturday, 7 November 2026
</p>
<a
@@ -78,12 +85,12 @@
<ul class="mt-4 flex flex-col gap-2">
{#each events as event (event.time)}
<li
class="border-surface-300 dark:border-surface-700 flex items-baseline gap-4 border-b py-2"
class="border-surface-50/30 flex items-baseline gap-4 border-b py-2"
>
<span class="text-secondary-600-400 w-16 shrink-0 text-2xl"
<span class="text-secondary-300 w-16 shrink-0 text-2xl"
>{event.time}</span
>
<span class="text-2xl">{event.label}</span>
<span class="text-surface-100 text-2xl">{event.label}</span>
</li>
{/each}
</ul>
+19 -12
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PhotoBackground from "$lib/components/PhotoBackground.svelte";
import { createWeddingInfo } from "$lib/wedding-info.svelte";
const wedding = createWeddingInfo();
@@ -10,13 +11,19 @@
);
</script>
<section class="flex flex-1 items-center justify-center px-4 py-16">
<div class="w-full max-w-md">
<h1 class="font-serif text-4xl">The Venue</h1>
<section
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16 z-10"
>
<PhotoBackground />
<div
class="border-surface-50/70 text-shadow-sm text-shadow-black/60 relative z-10 w-full max-w-md border px-6 py-12 sm:px-14 sm:py-16"
>
<h1 class="text-surface-50 font-serif text-4xl">The Venue</h1>
<div class="mt-6 flex flex-col gap-1">
<p class="text-2xl">{wedding.venueName}</p>
<p class="text-surface-600-400 text-2xl">{wedding.venueAddress}</p>
<p class="text-surface-100 text-2xl">{wedding.venueName}</p>
<p class="text-surface-200 text-2xl">{wedding.venueAddress}</p>
</div>
<a
@@ -28,17 +35,17 @@
Get directions
</a>
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-3xl">Parking</h2>
<p class="text-surface-600-400 mt-2 text-2xl">
<div class="border-surface-50/30 mt-10 border-t pt-6">
<h2 class="text-surface-50 font-serif text-3xl">Parking</h2>
<p class="text-surface-100 mt-2 text-2xl">
Secure parking is available on-site at the venue.
</p>
</div>
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-3xl">Accommodation</h2>
<p class="mt-2 text-2xl">Plaaskombuis Meyerton</p>
<div class="text-surface-600-400 mt-1 flex flex-col text-2xl">
<div class="border-surface-50/30 mt-10 border-t pt-6">
<h2 class="text-surface-50 font-serif text-3xl">Accommodation</h2>
<p class="text-surface-100 mt-2 text-2xl">Plaaskombuis Meyerton</p>
<div class="text-surface-200 mt-1 flex flex-col text-2xl">
<a href="tel:0163644144" class="hover:text-secondary-400">016 364 4144</a>
<a href="mailto:marisel@plaaskombuis.co.za" class="hover:text-secondary-400"
>marisel@plaaskombuis.co.za</a