package notification import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // ResendConfig configures the Resend HTTP sender. APIKey is required; // FromEmail is the sending address on a domain you've verified in the // Resend dashboard. type ResendConfig struct { APIKey string FromEmail string FromName string // HTTPClient overrides the default client (mostly so tests can point // at httptest.Server). Leave nil for production. HTTPClient *http.Client BaseURL string // overrideable for tests; defaults to https://api.resend.com } // ResendEmailSender posts emails through https://api.resend.com/emails. // Implements auth.EmailSender + GuestEmailDispatcher. type ResendEmailSender struct { cfg ResendConfig tpls *Templates from string client *http.Client url string } func NewResendEmailSender(cfg ResendConfig, tpls *Templates) (*ResendEmailSender, error) { if cfg.APIKey == "" { return nil, errors.New("resend: APIKey required") } if cfg.FromEmail == "" { return nil, errors.New("resend: FromEmail required") } from := cfg.FromEmail if cfg.FromName != "" { from = fmt.Sprintf("%s <%s>", cfg.FromName, cfg.FromEmail) } cli := cfg.HTTPClient if cli == nil { cli = &http.Client{Timeout: 15 * time.Second} } base := cfg.BaseURL if base == "" { base = "https://api.resend.com" } return &ResendEmailSender{cfg: cfg, tpls: tpls, from: from, client: cli, url: base + "/emails"}, nil } // --- auth.EmailSender --- func (s *ResendEmailSender) SendVerification(ctx context.Context, to, name, link string) error { _, err := s.sendTemplated(ctx, to, "Verify your GuestGuard email", TmplVerification, map[string]any{"Name": name, "Link": link}) return err } func (s *ResendEmailSender) SendPasswordReset(ctx context.Context, to, name, link string) error { _, err := s.sendTemplated(ctx, to, "Reset your GuestGuard password", TmplPasswordReset, map[string]any{"Name": name, "Link": link, "ExpiryHumane": "1 hour"}) return err } func (s *ResendEmailSender) SendCollaboratorInvite(ctx context.Context, to, inviterName, eventName, role, link string) error { _, err := s.sendTemplated(ctx, to, inviterName+" invited you to "+eventName, TmplCollaboratorInvite, map[string]any{ "InviterName": inviterName, "EventName": eventName, "Role": role, "Link": link, }) return err } func (s *ResendEmailSender) SendRSVPEditLink(ctx context.Context, to, guestName, eventName, link string) error { _, err := s.sendTemplated(ctx, to, "Edit your RSVP for "+eventName, TmplRSVPEditLink, map[string]any{ "GuestName": guestName, "EventName": eventName, "Link": link, }) return err } // --- GuestEmailDispatcher --- func (s *ResendEmailSender) SendGuest(ctx context.Context, to, subject string, name TemplateName, data map[string]any) (string, error) { return s.sendTemplated(ctx, to, subject, name, data) } // --- internals --- type resendRequest struct { From string `json:"from"` To []string `json:"to"` Subject string `json:"subject"` HTML string `json:"html"` Text string `json:"text"` } type resendResponse struct { ID string `json:"id"` Message string `json:"message,omitempty"` } func (s *ResendEmailSender) sendTemplated(ctx context.Context, to, subject string, name TemplateName, data map[string]any) (string, error) { if data == nil { data = map[string]any{} } data["Subject"] = subject html, text, err := s.tpls.Render(name, data) if err != nil { return "", err } body, _ := json.Marshal(resendRequest{ From: s.from, To: []string{to}, Subject: subject, HTML: html, Text: text, }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.url, bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+s.cfg.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := s.client.Do(req) if err != nil { return "", fmt.Errorf("resend: do: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) if resp.StatusCode >= 300 { return "", fmt.Errorf("resend: status %d: %s", resp.StatusCode, string(respBody)) } var parsed resendResponse if err := json.Unmarshal(respBody, &parsed); err != nil { return "", fmt.Errorf("resend: parse: %w", err) } return parsed.ID, nil }