Files
descrybe/apps/api/internal/email/resend.go
T

123 lines
3.3 KiB
Go
Raw Normal View History

package email
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type resendTransport struct {
apiKey string
client *http.Client
}
func newResendTransport(apiKey string, client *http.Client) *resendTransport {
if client == nil {
client = &http.Client{Timeout: 20 * time.Second}
}
return &resendTransport{apiKey: apiKey, client: client}
}
func (r *resendTransport) Name() string { return ProviderResend }
func (r *resendTransport) Send(ctx context.Context, from FromIdentity, msg Outbound) error {
to := strings.TrimSpace(msg.To)
if to == "" {
return ErrInvalidRecipient
}
payload := map[string]any{
"from": from.Formatted(),
"to": []string{to},
"subject": msg.Subject,
}
if strings.TrimSpace(msg.HTML) != "" {
payload["html"] = msg.HTML
}
if strings.TrimSpace(msg.Text) != "" {
payload["text"] = msg.Text
}
if strings.TrimSpace(from.ReplyTo) != "" {
payload["reply_to"] = from.ReplyTo
}
if len(msg.Headers) > 0 {
payload["headers"] = msg.Headers
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+r.apiKey)
req.Header.Set("Content-Type", "application/json")
res, err := r.client.Do(req)
if err != nil {
return fmt.Errorf("resend request failed")
}
defer res.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if res.StatusCode >= 300 {
return fmt.Errorf("resend api %d", res.StatusCode)
}
_ = raw
return nil
}
// verifyResendDomain checks the API key can list domains and that domain appears verified.
// When Resend returns no domains (sandbox), returns ok=false with a clear message.
func verifyResendDomain(ctx context.Context, apiKey, domain string, client *http.Client) (bool, string, error) {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain == "" {
return false, "domain required", nil
}
if client == nil {
client = &http.Client{Timeout: 15 * time.Second}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.resend.com/domains", nil)
if err != nil {
return false, "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
res, err := client.Do(req)
if err != nil {
return false, "", fmt.Errorf("resend domains request failed")
}
defer res.Body.Close()
raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return false, "", err
}
if res.StatusCode == http.StatusUnauthorized {
return false, "invalid Resend API key", nil
}
if res.StatusCode >= 300 {
return false, fmt.Sprintf("resend domains api %d", res.StatusCode), nil
}
var parsed struct {
Data []struct {
Name string `json:"name"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
return false, "unexpected resend response", nil
}
for _, d := range parsed.Data {
if strings.EqualFold(d.Name, domain) {
st := strings.ToLower(strings.TrimSpace(d.Status))
if st == "verified" || st == "ok" || st == "active" {
return true, "domain verified with Resend", nil
}
return false, fmt.Sprintf("Resend domain status is %q", d.Status), nil
}
}
return false, "domain not found in Resend account — add and verify it in the Resend dashboard", nil
}