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:
@@ -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
@@ -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")
|
||||
|
||||
@@ -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 & 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 & 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é & 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user