package mail
import (
"context"
"bytes"
"encoding/json"
"errors"
"net/http"
"time"
)
type Payload struct {
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"`
}
func Send(ctx context.Context, client *http.Client, endpoint, apiKey, idemKey string, p Payload) error {
b, err := json.Marshal(p)
if err != nil {
return err
}
cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(cctx, http.MethodPost, endpoint, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Idempotency-Key", idemKey)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.New("email provider returned non-2xx")
}
return nil
}
For transactional email, the reliability problem is usually latency and retries, not MIME formatting. I prefer an HTTP email provider because requests are easy to bound with context.WithTimeout and easier to observe than raw SMTP. The code below builds a JSON payload, sets Authorization and an Idempotency-Key, and sends it through a tuned http.Client. The idempotency key is important: if a job retries after a timeout, the provider can dedupe the send instead of delivering duplicates. I also treat non-2xx responses as errors and avoid logging the full email body to prevent leaking PII. In production, this call lives in a background worker and the API request returns quickly, but the same pattern applies: timeouts, explicit headers, and clear error handling keep email from turning into a tail-latency trap.