81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
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 <script>
|
|
// never reaches an inbox.
|
|
var md = goldmark.New()
|
|
|
|
func announcementMessage(a Announcement, publicURL string) (message, error) {
|
|
var buf bytes.Buffer
|
|
if err := md.Convert([]byte(a.BodyMarkdown), &buf); err != nil {
|
|
return message{}, fmt.Errorf("mail: markdown: %w", err)
|
|
}
|
|
return render("announcement", announcementData{Announcement: a, BodyHTML: htmltmpl.HTML(buf.String())}, publicURL)
|
|
}
|
|
|
|
// RenderAnnouncement is the staff preview: exactly what a recipient gets.
|
|
func RenderAnnouncement(a Announcement, publicURL string) (subject, html, text string, err error) {
|
|
m, err := announcementMessage(a, publicURL)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return m.Subject, m.HTML, m.Text, nil
|
|
}
|
|
|
|
// SendAnnouncement sends one announcement to one address, with the
|
|
// List-Unsubscribe pair Gmail and Yahoo require of bulk senders.
|
|
func (s Sender) SendAnnouncement(to string, a Announcement) error {
|
|
m, err := s.announcementFor(to, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.send(m)
|
|
}
|
|
|
|
// announcementFor builds the one-recipient message SendAnnouncement puts on
|
|
// the wire. Every refusal is a *SendError at PhasePrepare: it is about this
|
|
// one message, never the mail server, so a caller moves on to the next row.
|
|
func (s Sender) announcementFor(to string, a Announcement) (message, error) {
|
|
if strings.Contains(to, ",") {
|
|
return message{}, &SendError{Phase: PhasePrepare, Err: fmt.Errorf("mail: an announcement goes to exactly one address")}
|
|
}
|
|
if a.UnsubscribeURL == "" {
|
|
return message{}, &SendError{Phase: PhasePrepare, Err: fmt.Errorf("mail: announcement without an unsubscribe URL")}
|
|
}
|
|
m, err := announcementMessage(a, s.PublicURL)
|
|
if err != nil {
|
|
return message{}, &SendError{Phase: PhasePrepare, Err: err}
|
|
}
|
|
m.To = to
|
|
m.Headers = map[string]string{
|
|
"List-Unsubscribe": "<" + a.UnsubscribeURL + ">",
|
|
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
|
}
|
|
return m, nil
|
|
}
|