package mail import ( "context" _ "embed" "fmt" "html" "os" "strings" "github.com/resend/resend-go/v3" ) //go:embed mark.png var markPNG []byte // QuestionAnswered is the payload for notifying a question author of a reply. type QuestionAnswered struct { ToEmail string ToName string QuestionID string QuestionTitle string AnswerBody string } // Notifier sends transactional email about answered questions. type Notifier interface { NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswered) error } // Nop is a no-op Notifier used when Resend is not configured. type Nop struct{} func (Nop) NotifyQuestionAnswered(context.Context, QuestionAnswered) 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) NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswered) error { if r == nil || r.client == nil { return nil } to := strings.TrimSpace(msg.ToEmail) if to == "" { return nil } text, htmlBody := questionAnsweredContent(r.baseURL, msg) params := &resend.SendEmailRequest{ From: r.from, To: []string{to}, Subject: "Your question was answered", Text: text, Html: htmlBody, Attachments: []*resend.Attachment{{ Content: markPNG, Filename: "ask-a-plumber-first.png", ContentType: "image/png", ContentId: "answer-notification-mark", }}, } opts := &resend.SendEmailOptions{ IdempotencyKey: "answer-notify:" + msg.QuestionID, } _, err := r.client.Emails.SendWithOptions(ctx, params, opts) return err } func questionAnsweredContent(baseURL string, msg QuestionAnswered) (string, string) { link := strings.TrimRight(baseURL, "/") + "/questions/" + msg.QuestionID title := strings.TrimSpace(msg.QuestionTitle) if title == "" { title = "your question" } text := fmt.Sprintf( "Hi%s,\n\nYour question %q has an answer from a plumber:\n\n%s\n\nView it here:\n%s\n", greetingName(msg.ToName), title, msg.AnswerBody, link, ) htmlBody := strings.NewReplacer( "{{PREHEADER}}", html.EscapeString("A plumber answered "+title+"."), "{{GREETING}}", html.EscapeString(greetingName(msg.ToName)), "{{TITLE}}", html.EscapeString(title), "{{ANSWER}}", html.EscapeString(msg.AnswerBody), "{{LINK}}", html.EscapeString(link), "{{MARK}}", "cid:answer-notification-mark", ).Replace(questionAnsweredHTML) return text, htmlBody } const questionAnsweredHTML = ` Your question was answered
{{PREHEADER}}
Ask a Plumber First
Bay Area · Shop Dispatch
Shop response

Your question has an answer.

Hi{{GREETING}}, a plumber replied to:

“{{TITLE}}”

The answer
{{ANSWER}}
View the answer →
You received this because you asked a question on Ask a Plumber First.
` func greetingName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } return " " + name }