169 lines
4.8 KiB
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
|
|
}
|