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 }