package mail import ( "context" _ "embed" "fmt" "html" "net/url" "os" "strings" "github.com/resend/resend-go/v3" ) //go:embed mark.png var markPNG []byte // PostReply is the payload for notifying a post author of a direct reply. type PostReply struct { ToEmail string ToName string RootID string RootTitle string ReplyID string ReplyBody string ReplyAuthorName string } // Notifier sends transactional email about post replies. type Notifier interface { NotifyPostReply(ctx context.Context, msg PostReply) error } // Nop is a no-op Notifier used when Resend is not configured. type Nop struct{} func (Nop) NotifyPostReply(context.Context, PostReply) error { return nil } // Resend sends via the Resend HTTP API. type Resend struct { client *resend.Client from string baseURL string } // FromEnv builds a Notifier from RESEND_* and APP_BASE_URL. // Returns Nop when RESEND_API_KEY is unset. func FromEnv() (Notifier, error) { key := strings.TrimSpace(os.Getenv("RESEND_API_KEY")) if key == "" { return Nop{}, nil } from := strings.TrimSpace(os.Getenv("RESEND_FROM")) base := strings.TrimRight(strings.TrimSpace(os.Getenv("APP_BASE_URL")), "/") if from == "" { return nil, fmt.Errorf("RESEND_FROM is required when RESEND_API_KEY is set") } if base == "" { return nil, fmt.Errorf("APP_BASE_URL is required when RESEND_API_KEY is set") } return &Resend{ client: resend.NewClient(key), from: from, baseURL: base, }, nil } func (r *Resend) NotifyPostReply(ctx context.Context, msg PostReply) error { if r == nil || r.client == nil { return nil } to := strings.TrimSpace(msg.ToEmail) if to == "" { return nil } text, htmlBody := postReplyContent(r.baseURL, msg) params := &resend.SendEmailRequest{ From: r.from, To: []string{to}, Subject: "New reply to your post", Text: text, Html: htmlBody, Attachments: []*resend.Attachment{{ Content: markPNG, Filename: "ask-a-plumber-first.png", ContentType: "image/png", ContentId: "reply-notification-mark", }}, } opts := &resend.SendEmailOptions{ IdempotencyKey: "post-reply:" + msg.ReplyID, } _, err := r.client.Emails.SendWithOptions(ctx, params, opts) return err } func postReplyContent(baseURL string, msg PostReply) (string, string) { link := strings.TrimRight(baseURL, "/") + "/questions/" + url.PathEscape(msg.RootID) + "#post-" + url.PathEscape(msg.ReplyID) title := replyRootTitle(msg.RootTitle) author := strings.TrimSpace(msg.ReplyAuthorName) if author == "" { author = "Someone" } text := fmt.Sprintf( "Hi%s,\n\n%s replied in %q:\n\n%s\n\nView the reply:\n%s\n", greetingName(msg.ToName), author, title, msg.ReplyBody, link, ) htmlBody := strings.NewReplacer( "{{PREHEADER}}", html.EscapeString(author+" replied in "+title+"."), "{{GREETING}}", html.EscapeString(greetingName(msg.ToName)), "{{TITLE}}", html.EscapeString(title), "{{AUTHOR}}", html.EscapeString(author), "{{REPLY}}", html.EscapeString(msg.ReplyBody), "{{LINK}}", html.EscapeString(link), "{{MARK}}", "cid:reply-notification-mark", ).Replace(postReplyHTML) return text, htmlBody } func replyRootTitle(title string) string { title = strings.TrimSpace(title) if title == "" { return "your conversation" } return title } const postReplyHTML = ` New reply to your conversation
{{PREHEADER}}
Ask a Plumber First
Bay Area · Shop Dispatch
New reply

The conversation has a new reply.

Hi{{GREETING}}, {{AUTHOR}} replied in:

“{{TITLE}}”

The reply
{{REPLY}}
View the reply →
You received this because someone replied to your post on Ask a Plumber First.
` func greetingName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } return " " + name }