feat: email service — SMTP + deadline reminders + invitations (t-paliad-021)
- internal/services/mail_service.go: SMTP/TLS sender (implicit TLS on 465), html/template rendering, branded base layout + content templates, silent no-op when SMTP_* unset. - internal/services/reminder_service.go: hourly scanner for Fristen that are overdue / due tomorrow / due within the week (Monday digest). Dedup via paliad.reminder_log (24h window). - internal/services/invite_service.go: POST /api/invite flow with domain whitelist, in-memory 10/day/user rate limit, audit row in paliad.invitations. - internal/handlers/invite.go: POST + GET /api/invite handlers. - Sidebar "Kolleg:in einladen" button + modal on every page. - migration 016: paliad.reminder_log, paliad.invitations, users.lang column. - docker-compose: SMTP_* + PALIAD_BASE_URL env vars. - docs/feature-roadmap.md: documented Supabase auth-SMTP routing as open question; current pilot keeps identity mails on Supabase default sender. Rationale: get Paliad off Supabase's best-effort outbound for the inbox-facing stuff (reminders, invitations) and move deadline nudges from passive dashboard to active email. Custom Supabase auth SMTP is blocked on the shared ydb.youpc.org instance — deferred until Paliad has its own project or GoTrue webhook relay.
This commit is contained in:
15
internal/templates/email.go
Normal file
15
internal/templates/email.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package templates exposes the embedded email HTML templates. The package
|
||||
// exists so that //go:embed can reach the files from a stable location —
|
||||
// embed directives can't use "..", so every consumer (MailService, tests)
|
||||
// reads through EmailFS instead of crossing package boundaries with relative
|
||||
// paths.
|
||||
package templates
|
||||
|
||||
import "embed"
|
||||
|
||||
// EmailFS contains every HTML template under internal/templates/email/.
|
||||
// The MailService uses these as {{define "content"}} overrides layered onto
|
||||
// base.html; adding a new email means dropping the file in and parsing it.
|
||||
//
|
||||
//go:embed email/*.html
|
||||
var EmailFS embed.FS
|
||||
38
internal/templates/email/base.html
Normal file
38
internal/templates/email/base.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{.Lang}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Subject}}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f5f5f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;color:#1c1917;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f5f5f4;padding:24px 0;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="560" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
|
||||
<tr>
|
||||
<td style="background:#c6f41c;padding:20px 28px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="left" style="font-size:20px;font-weight:700;color:#1c1917;letter-spacing:-0.01em;">
|
||||
<span style="display:inline-block;width:28px;height:28px;background:#1c1917;color:#c6f41c;border-radius:6px;text-align:center;line-height:28px;font-weight:800;vertical-align:middle;margin-right:10px;">p</span>
|
||||
<span style="vertical-align:middle;">Paliad</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:32px 28px;font-size:15px;line-height:1.55;color:#1c1917;">
|
||||
{{block "content" .}}{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:18px 28px;border-top:1px solid #e7e5e4;font-size:12px;color:#78716c;text-align:center;">
|
||||
Paliad — <a href="https://paliad.de" style="color:#78716c;text-decoration:none;">paliad.de</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
42
internal/templates/email/deadline_reminder.html
Normal file
42
internal/templates/email/deadline_reminder.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{define "content"}}
|
||||
{{if eq .Lang "en"}}
|
||||
{{if eq .Kind "overdue"}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#b91c1c;">Deadline overdue</h1>
|
||||
<p style="margin:0 0 12px 0;">The following deadline was due <strong>today or earlier</strong> and is still open:</p>
|
||||
{{else}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">Deadline tomorrow</h1>
|
||||
<p style="margin:0 0 12px 0;">The following deadline is due <strong>tomorrow</strong>:</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if eq .Kind "overdue"}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#b91c1c;">Frist überfällig</h1>
|
||||
<p style="margin:0 0 12px 0;">Die folgende Frist war <strong>heute oder früher</strong> fällig und ist noch offen:</p>
|
||||
{{else}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">Frist morgen fällig</h1>
|
||||
<p style="margin:0 0 12px 0;">Die folgende Frist ist <strong>morgen</strong> fällig:</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;border:1px solid #e7e5e4;border-radius:6px;">
|
||||
<tr>
|
||||
<td style="padding:16px;">
|
||||
<div style="font-weight:600;font-size:16px;margin-bottom:6px;">{{.Title}}</div>
|
||||
<div style="color:#57534e;font-size:13px;margin-bottom:10px;">
|
||||
{{if eq .Lang "en"}}Due:{{else}}Fällig am:{{end}}
|
||||
<strong style="color:#1c1917;">{{.DueDate}}</strong>
|
||||
</div>
|
||||
<div style="color:#57534e;font-size:13px;">
|
||||
{{if eq .Lang "en"}}Matter:{{else}}Akte:{{end}}
|
||||
<strong style="color:#1c1917;">{{.AkteAktenzeichen}}</strong>
|
||||
{{if .AkteTitle}} — {{.AkteTitle}}{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="margin:20px 0 0 0;">
|
||||
<a href="{{.FristURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
{{if eq .Lang "en"}}Open in Paliad{{else}}In Paliad öffnen{{end}}
|
||||
</a>
|
||||
</p>
|
||||
{{end}}
|
||||
38
internal/templates/email/deadline_weekly.html
Normal file
38
internal/templates/email/deadline_weekly.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{{define "content"}}
|
||||
{{if eq .Lang "en"}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">Deadlines this week</h1>
|
||||
<p style="margin:0 0 16px 0;">You have <strong>{{.Count}}</strong> open deadline{{if ne .Count 1}}s{{end}} in the next 7 days:</p>
|
||||
{{else}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">Wochenübersicht Fristen</h1>
|
||||
<p style="margin:0 0 16px 0;">Sie haben <strong>{{.Count}}</strong> offene Frist{{if ne .Count 1}}en{{end}} in den nächsten 7 Tagen:</p>
|
||||
{{end}}
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:8px 0 16px 0;border:1px solid #e7e5e4;border-radius:6px;border-collapse:separate;border-spacing:0;">
|
||||
<tr style="background:#f5f5f4;">
|
||||
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">
|
||||
{{if eq .Lang "en"}}Due{{else}}Fällig{{end}}
|
||||
</th>
|
||||
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">
|
||||
{{if eq .Lang "en"}}Title{{else}}Titel{{end}}
|
||||
</th>
|
||||
<th align="left" style="padding:10px 12px;font-size:12px;color:#57534e;font-weight:600;border-bottom:1px solid #e7e5e4;">
|
||||
{{if eq .Lang "en"}}Matter{{else}}Akte{{end}}
|
||||
</th>
|
||||
</tr>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #f5f5f4;white-space:nowrap;{{if .Overdue}}color:#b91c1c;font-weight:600;{{end}}">{{.DueDate}}</td>
|
||||
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #f5f5f4;">
|
||||
<a href="{{.URL}}" style="color:#1c1917;text-decoration:none;font-weight:500;">{{.Title}}</a>
|
||||
</td>
|
||||
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #f5f5f4;">{{.AkteAktenzeichen}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<p style="margin:20px 0 0 0;">
|
||||
<a href="{{.FristenURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
{{if eq .Lang "en"}}All deadlines{{else}}Alle Fristen{{end}}
|
||||
</a>
|
||||
</p>
|
||||
{{end}}
|
||||
29
internal/templates/email/invitation.html
Normal file
29
internal/templates/email/invitation.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "content"}}
|
||||
{{if eq .Lang "en"}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">{{.InviterName}} invites you to Paliad</h1>
|
||||
<p style="margin:0 0 12px 0;">Paliad is the patent practice platform for HLC — matter management, deadline calculations, knowledge tools, and more.</p>
|
||||
{{if .Message}}
|
||||
<div style="background:#f5f5f4;border-left:3px solid #c6f41c;padding:14px 16px;margin:20px 0;border-radius:0 6px 6px 0;font-size:14px;line-height:1.55;color:#44403c;white-space:pre-wrap;">{{.Message}}</div>
|
||||
{{end}}
|
||||
<p style="margin:20px 0 24px 0;">Sign up with your HLC email to get started:</p>
|
||||
<p style="margin:0;">
|
||||
<a href="{{.RegisterURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
Join Paliad
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin:24px 0 0 0;font-size:12px;color:#78716c;">Sent to {{.ToEmail}} by {{.InviterEmail}}. If you didn't expect this invitation, you can ignore it.</p>
|
||||
{{else}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">{{.InviterName}} lädt Sie zu Paliad ein</h1>
|
||||
<p style="margin:0 0 12px 0;">Paliad ist die Patent-Praxis-Plattform für HLC — Aktenverwaltung, Fristenberechnung, Wissenswerkzeuge und mehr.</p>
|
||||
{{if .Message}}
|
||||
<div style="background:#f5f5f4;border-left:3px solid #c6f41c;padding:14px 16px;margin:20px 0;border-radius:0 6px 6px 0;font-size:14px;line-height:1.55;color:#44403c;white-space:pre-wrap;">{{.Message}}</div>
|
||||
{{end}}
|
||||
<p style="margin:20px 0 24px 0;">Registrieren Sie sich mit Ihrer HLC-E-Mail-Adresse:</p>
|
||||
<p style="margin:0;">
|
||||
<a href="{{.RegisterURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
Zu Paliad anmelden
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin:24px 0 0 0;font-size:12px;color:#78716c;">Gesendet an {{.ToEmail}} von {{.InviterEmail}}. Falls Sie diese Einladung nicht erwartet haben, können Sie sie ignorieren.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user