learn-platform-deploy/Backend/cmd/api/notifications_sendgrid.go

169 lines
4.8 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
)
type notificationHTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type notificationProviderProfile struct {
Provider string
APIKey string
FromEmail string
APIBaseURL string
}
type sendGridNotificationProvider struct {
apiKey string
fromEmail string
apiBaseURL string
httpClient notificationHTTPClient
}
type sendGridEmailAddress struct {
Email string `json:"email"`
}
type sendGridPersonalization struct {
To []sendGridEmailAddress `json:"to"`
DynamicTemplateData map[string]any `json:"dynamic_template_data,omitempty"`
}
type sendGridContent struct {
Type string `json:"type"`
Value string `json:"value"`
}
type sendGridMailPayload struct {
Personalizations []sendGridPersonalization `json:"personalizations"`
From sendGridEmailAddress `json:"from"`
Subject string `json:"subject,omitempty"`
Content []sendGridContent `json:"content,omitempty"`
TemplateID string `json:"template_id,omitempty"`
}
func newNotificationProvider(cfg config) notificationProvider {
return newNotificationProviderFromProfile(notificationProviderProfile{
Provider: cfg.NotificationProvider,
APIKey: cfg.SendGridAPIKey,
FromEmail: cfg.SendGridFromEmail,
APIBaseURL: cfg.SendGridAPIBaseURL,
})
}
func newNotificationProviderFromProfile(profile notificationProviderProfile) notificationProvider {
provider := strings.TrimSpace(strings.ToLower(profile.Provider))
if provider == "" || provider == "noop" {
return noopNotificationProvider{}
}
if provider != "sendgrid" {
log.Printf("unknown NOTIFICATION_PROVIDER=%q; falling back to noop", profile.Provider)
return noopNotificationProvider{}
}
apiKey := strings.TrimSpace(profile.APIKey)
fromEmail := strings.TrimSpace(profile.FromEmail)
if apiKey == "" || fromEmail == "" {
log.Printf("sendgrid provider selected but missing SENDGRID_API_KEY or SENDGRID_FROM_EMAIL; falling back to noop")
return noopNotificationProvider{}
}
apiBaseURL := strings.TrimSpace(profile.APIBaseURL)
if apiBaseURL == "" {
apiBaseURL = "https://api.sendgrid.com"
}
return sendGridNotificationProvider{
apiKey: apiKey,
fromEmail: fromEmail,
apiBaseURL: strings.TrimRight(apiBaseURL, "/"),
httpClient: http.DefaultClient,
}
}
func (s sendGridNotificationProvider) Metadata() notificationProviderMetadata {
return notificationProviderMetadata{
Name: "sendgrid",
Capabilities: notificationProviderCapabilities{
Email: true,
Templates: true,
Webhooks: true,
Events: true,
},
}
}
func (s sendGridNotificationProvider) Send(ctx context.Context, request notificationRequest) (notificationProviderResult, error) {
if strings.TrimSpace(strings.ToLower(request.Channel)) != "email" {
return notificationProviderResult{}, errors.New("sendgrid provider supports email channel only")
}
if strings.TrimSpace(request.To) == "" {
return notificationProviderResult{}, errors.New("notification destination is required")
}
payload := sendGridMailPayload{
Personalizations: []sendGridPersonalization{
{
To: []sendGridEmailAddress{{Email: strings.TrimSpace(request.To)}},
DynamicTemplateData: request.Metadata,
},
},
From: sendGridEmailAddress{Email: s.fromEmail},
Subject: strings.TrimSpace(request.Subject),
TemplateID: strings.TrimSpace(request.TemplateID),
}
if strings.TrimSpace(request.Body) != "" {
payload.Content = []sendGridContent{{Type: "text/plain", Value: request.Body}}
}
body, err := json.Marshal(payload)
if err != nil {
return notificationProviderResult{}, fmt.Errorf("marshal sendgrid payload: %w", err)
}
endpoint := strings.TrimRight(s.apiBaseURL, "/") + "/v3/mail/send"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return notificationProviderResult{}, fmt.Errorf("create sendgrid request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+s.apiKey)
req.Header.Set("Content-Type", "application/json")
client := s.httpClient
if client == nil {
client = http.DefaultClient
}
res, err := client.Do(req)
if err != nil {
return notificationProviderResult{}, fmt.Errorf("sendgrid request failed: %w", err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode > 299 {
raw, _ := io.ReadAll(io.LimitReader(res.Body, 8192))
return notificationProviderResult{}, fmt.Errorf("sendgrid send failed: status=%d body=%s", res.StatusCode, strings.TrimSpace(string(raw)))
}
messageID := strings.TrimSpace(res.Header.Get("X-Message-Id"))
if messageID == "" {
messageID = "sendgrid-accepted"
}
return notificationProviderResult{
ProviderMessageID: messageID,
Status: "queued",
}, nil
}