84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/mail"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.sendico.io/tech/sxchnge/notifications/model"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
sharedmodel "git.sendico.io/tech/sxchnge/shared/model"
|
|
"github.com/go-viper/mapstructure/v2"
|
|
)
|
|
|
|
type Driver string
|
|
|
|
const (
|
|
SendGrid Driver = "sendgrid"
|
|
Resend Driver = "resend"
|
|
)
|
|
|
|
type Config = sharedmodel.DriverConfig[Driver]
|
|
type Settings struct {
|
|
APIKeyEnv string `mapstructure:"api_key_env"`
|
|
SenderAddress string `mapstructure:"sender_address"`
|
|
SenderName string `mapstructure:"sender_name"`
|
|
}
|
|
|
|
// Outcome describes provider acceptance, never recipient delivery or reading.
|
|
type Outcome struct {
|
|
State model.State
|
|
Code string
|
|
RetryAfter time.Duration
|
|
// Provider diagnostics are sanitized operational data, not public failures.
|
|
HTTPStatus int
|
|
ProviderError string
|
|
ProviderMessage string
|
|
}
|
|
type Sender interface {
|
|
Send(context.Context, string, model.Email) Outcome
|
|
}
|
|
|
|
type client struct {
|
|
http *http.Client
|
|
key string
|
|
settings Settings
|
|
endpoint string
|
|
}
|
|
|
|
func New(config Config) (Sender, error) {
|
|
var s Settings
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{Result: &s, ErrorUnused: true, TagName: "mapstructure"})
|
|
if err != nil {
|
|
return nil, merrors.InternalWrap(err, "create driver settings decoder")
|
|
}
|
|
if err = decoder.Decode(config.Settings); err != nil {
|
|
return nil, merrors.InvalidArgument("invalid email driver settings")
|
|
}
|
|
if !ValidAddress(s.SenderAddress) || strings.ContainsAny(s.SenderName, "\r\n") {
|
|
return nil, merrors.InvalidArgument("invalid sender")
|
|
}
|
|
key := os.Getenv(s.APIKeyEnv)
|
|
if s.APIKeyEnv == "" || strings.TrimSpace(key) == "" || strings.ContainsAny(key, "\r\n") {
|
|
return nil, merrors.FailedPrecondition("email API key environment variable is missing")
|
|
}
|
|
c := client{http: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, key: key, settings: s}
|
|
switch config.Driver {
|
|
case SendGrid:
|
|
c.endpoint = "https://api.sendgrid.com/v3/mail/send"
|
|
return &sendgrid{c}, nil
|
|
case Resend:
|
|
c.endpoint = "https://api.resend.com/emails"
|
|
return &resend{c}, nil
|
|
default:
|
|
return nil, merrors.InvalidArgument("unsupported email driver")
|
|
}
|
|
}
|
|
func ValidAddress(s string) bool {
|
|
a, err := mail.ParseAddress(s)
|
|
return err == nil && a.Address == s && !strings.ContainsAny(s, "\r\n") && len(s) <= 254
|
|
}
|