package mail import ( "context" "fmt" "html" "os" "strings" "github.com/resend/resend-go/v3" ) // 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 } link := r.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 := fmt.Sprintf( `
Hi%s,
Your question %s has an answer from a plumber:
%s`, html.EscapeString(greetingName(msg.ToName)), html.EscapeString(title), html.EscapeString(msg.AnswerBody), html.EscapeString(link), ) params := &resend.SendEmailRequest{ From: r.from, To: []string{to}, Subject: "Your question was answered", Text: text, Html: htmlBody, } opts := &resend.SendEmailOptions{ IdempotencyKey: "answer-notify:" + msg.QuestionID, } _, err := r.client.Emails.SendWithOptions(ctx, params, opts) return err } func greetingName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } return " " + name }