mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 23:15:40 +00:00
cec056cefe
* feat(email): cloud-mode marketing footer in transactional emails (TASK-907)
Extracts a shared HTML/plain shell helper for the five existing
transactional-email templates (SendInvitation, SendWelcome,
SendPasswordReset, SendPaymentFailed, SendTest) and adds a Cloud-only
marketing footer that mirrors the auth-page AuthFooter component:
GitHub / Docs / Changelog / Privacy / Terms link list plus a
"© <year> Pad · Perpetual Software" copyright line.
Self-hosted output (the default for any pad instance NOT in
PAD_CLOUD/PAD_MODE=cloud) is byte-equivalent to the prior inline
templates: same wordmark header, same body, same footer-note disclosure,
no marketing links. Operators ship Pad under their own brand and
getpad.dev's link list would be wrong on their notifications.
Plumbing:
- email.Sender gains a cloudMode bool + SetCloudMode/CloudMode
accessors. Configure() does not touch cloudMode (it's set
independently from API-key/from-addr config).
- Server.SetCloudMode now propagates to s.email.SetCloudMode(true)
so existing senders pick up the flag.
- Server.SetEmailSender propagates s.cloudMode → e.cloudMode when
email is wired AFTER cloud mode (handles the cmd/pad/main.go
ordering where SetEmailSender is called from main).
- Server.reconfigureEmail() (admin-settings reload path) does the
same so an admin reconfiguring email mid-flight doesn't end up
with a sender stuck in self-hosted mode.
The email accent color (#2563eb) is preserved from the prior templates
— it has known contrast properties on white email backgrounds. Email
is light-themed for cross-client readability; the dark-theme tokens
from docs/brand.md §3 are for in-app/auth surfaces, not transactional
mail.
Pinned with three regression tests:
- self-hosted shell renders no Cloud-only markers
- Cloud shell renders the link list in canonical order (GitHub →
Docs → Changelog → Privacy → Terms)
- plain-text shell branches identically
Visual contract: docs/brand.md §7 (link order) and §6 (Pad wordmark).
Companion to AuthHeader, AuthFooter, +error.svelte, and UserMenuResources
already shipped on PLAN-900.
Test plan:
- go build ./... — clean
- go vet ./... — clean
- go test ./... — all pass (including new shell_test.go cases)
- web/npm run check — 0 errors
- web/npm run build — clean
* fix(email): full canonical link list per Codex (round 2)
Codex caught that the Cloud-mode email footer carried only 5 of the 9
canonical links from docs/brand.md §7 (GitHub / Docs / Changelog /
Privacy / Terms — omitted Contribute / FAQ / Security / Sub-processors).
The brand spec §1 says transactional emails get "Full parity" with the
auth-page AuthFooter; my trim violated that contract.
Add the four missing links to both the HTML and plain-text shells in
the canonical order: GitHub → Docs → Changelog → Contribute → FAQ →
Security → Privacy → Terms → Sub-processors. Update the regression
tests to pin all 9 markers + their pairwise ordering.
The "keep emails small" instinct that motivated the trim was a real
design concern but not strong enough to defy the brand spec. If we
later decide email needs a reduced subset, the right move is to
update §7 in docs/brand.md FIRST (acknowledging email as a surface
with a smaller link list) and trim the implementation to match.
122 lines
5.0 KiB
Go
122 lines
5.0 KiB
Go
package email
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// buildHTMLShell wraps body HTML in the standard transactional-email
|
|
// chrome — header (Pad wordmark) + body + footer note + (Cloud only)
|
|
// marketing footer (copyright line + canonical link list).
|
|
//
|
|
// Self-hosted output is byte-equivalent to the prior inline templates so
|
|
// operators see no visible change after this refactor — they ship Pad
|
|
// under their own brand and getpad.dev marketing links would be wrong on
|
|
// their notifications.
|
|
//
|
|
// Visual contract: docs/brand.md §6 (header) + §7 (footer link order).
|
|
// Email is light-themed for cross-client readability — emails generally
|
|
// don't follow the dark theme of the in-app surfaces. The accent color
|
|
// (#2563eb) is kept from the prior templates because it has known
|
|
// contrast properties on light backgrounds.
|
|
//
|
|
// The bodyHTML parameter is interpolated verbatim — callers must
|
|
// HTML-escape any user-controlled content before passing it in. The
|
|
// footerNote is a single-paragraph "you received this because…"
|
|
// disclosure rendered in the small grey footer text directly above
|
|
// the optional marketing block.
|
|
func buildHTMLShell(bodyHTML, footerNoteHTML string, cloudMode bool) string {
|
|
var b strings.Builder
|
|
b.WriteString(`<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"></head>
|
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a1a; max-width: 560px; margin: 0 auto; padding: 40px 20px;">
|
|
<div style="margin-bottom: 32px;">
|
|
<strong style="font-size: 18px;">Pad</strong>
|
|
</div>
|
|
`)
|
|
b.WriteString(bodyHTML)
|
|
b.WriteString(` <hr style="border: none; border-top: 1px solid #e5e5e5; margin: 32px 0;" />
|
|
`)
|
|
if footerNoteHTML != "" {
|
|
b.WriteString(` <p style="font-size: 12px; color: #999;">
|
|
`)
|
|
b.WriteString(footerNoteHTML)
|
|
b.WriteString(`
|
|
</p>
|
|
`)
|
|
}
|
|
if cloudMode {
|
|
// Marketing footer block — full canonical link list from docs/brand.md
|
|
// §7, in the order the brand spec mandates: GitHub, Docs, Changelog,
|
|
// Contribute, FAQ, Security, Privacy, Terms, Sub-processors. The
|
|
// auth-page AuthFooter component carries the same nine links; emails
|
|
// get the same set so the brand spec's "Full parity" promise for
|
|
// transactional mail (§1) is honored. Codex caught a trimmed subset
|
|
// on first review (TASK-907 round 1).
|
|
year := time.Now().UTC().Year()
|
|
b.WriteString(fmt.Sprintf(` <p style="font-size: 11px; color: #999; line-height: 1.5; margin-top: 16px;">
|
|
<a href="https://github.com/PerpetualSoftware/pad" style="color: #999; text-decoration: underline;">GitHub</a>
|
|
·
|
|
<a href="https://getpad.dev/docs" style="color: #999; text-decoration: underline;">Docs</a>
|
|
·
|
|
<a href="https://getpad.dev/changelog" style="color: #999; text-decoration: underline;">Changelog</a>
|
|
·
|
|
<a href="https://getpad.dev/contribute" style="color: #999; text-decoration: underline;">Contribute</a>
|
|
·
|
|
<a href="https://getpad.dev/faq" style="color: #999; text-decoration: underline;">FAQ</a>
|
|
·
|
|
<a href="https://getpad.dev/security" style="color: #999; text-decoration: underline;">Security</a>
|
|
·
|
|
<a href="https://getpad.dev/privacy" style="color: #999; text-decoration: underline;">Privacy</a>
|
|
·
|
|
<a href="https://getpad.dev/terms" style="color: #999; text-decoration: underline;">Terms</a>
|
|
·
|
|
<a href="https://getpad.dev/subprocessors" style="color: #999; text-decoration: underline;">Sub-processors</a>
|
|
</p>
|
|
<p style="font-size: 11px; color: #aaa; margin-top: 8px;">
|
|
© %d Pad · Perpetual Software
|
|
</p>
|
|
`, year))
|
|
}
|
|
b.WriteString(`</body>
|
|
</html>`)
|
|
return b.String()
|
|
}
|
|
|
|
// buildPlainShell builds the plain-text body for transactional emails.
|
|
// Self-hosted output is byte-equivalent to the prior templates. Cloud
|
|
// output adds a small marketing-link block and copyright line below the
|
|
// per-template footer note. Plain text intentionally stays minimal —
|
|
// no chrome, no decorative separators beyond the dashed rule.
|
|
func buildPlainShell(bodyText, footerNoteText string, cloudMode bool) string {
|
|
var b strings.Builder
|
|
b.WriteString(bodyText)
|
|
if footerNoteText != "" {
|
|
b.WriteString("\n\n---\n")
|
|
b.WriteString(footerNoteText)
|
|
}
|
|
if cloudMode {
|
|
// Same nine canonical links as the HTML branch (docs/brand.md §7).
|
|
// Plain text uses fixed-width labels for legibility in monospaced
|
|
// mail clients; URL alignment doesn't matter visually but reads
|
|
// cleanly when piped through a screen reader.
|
|
year := time.Now().UTC().Year()
|
|
b.WriteString(fmt.Sprintf(`
|
|
|
|
GitHub: https://github.com/PerpetualSoftware/pad
|
|
Docs: https://getpad.dev/docs
|
|
Changelog: https://getpad.dev/changelog
|
|
Contribute: https://getpad.dev/contribute
|
|
FAQ: https://getpad.dev/faq
|
|
Security: https://getpad.dev/security
|
|
Privacy: https://getpad.dev/privacy
|
|
Terms: https://getpad.dev/terms
|
|
Sub-processors: https://getpad.dev/subprocessors
|
|
|
|
(c) %d Pad — Perpetual Software`, year))
|
|
}
|
|
return b.String()
|
|
}
|