diff --git a/go.mod b/go.mod index 3bc0bb1..70996dc 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yuin/goldmark v1.8.6 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 68a74c1..8f81fc0 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8= go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/mail/announcement.go b/mail/announcement.go new file mode 100644 index 0000000..afc4316 --- /dev/null +++ b/mail/announcement.go @@ -0,0 +1,69 @@ +package mail + +import ( + "bytes" + "fmt" + htmltmpl "html/template" + "strings" + + "github.com/yuin/goldmark" +) + +// Announcement is one product email to one person: a feature launch, a guide, +// an offer. vantage-admin's announce package builds it per recipient, because +// the unsubscribe links carry that person's token. +type Announcement struct { + CategoryLabel string // "New features" + Subject string + BodyMarkdown string + UnsubscribeURL string // RFC 8058 one-click target for this category + PreferencesURL string // the no-login preferences page + PostalAddress string // optional footer line +} + +type announcementData struct { + Announcement + BodyHTML htmltmpl.HTML +} + +// md renders with goldmark's defaults, which omit raw HTML: a pasted \n\n- cron syntax\n- time zones", + UnsubscribeURL: "https://api.example/email/unsubscribe?c=features&t=abc", + PreferencesURL: "https://hq.example/email-preferences?t=abc", + PostalAddress: "Hostxtra Ltd, 1 High Street, Leeds", + } +} + +func TestRenderAnnouncement(t *testing.T) { + subject, html, text, err := RenderAnnouncement(sampleAnnouncement(), "https://hq.example") + if err != nil { + t.Fatal(err) + } + if subject != "New in Vantage: scheduled workflows" { + t.Errorf("subject = %q", subject) + } + if !strings.Contains(html, "on a schedule") { + t.Error("markdown not rendered to html") + } + if strings.Contains(html, "