Documentation
¶
Overview ¶
Package email is a pluggable transactional-email kit. A single Sender interface fronts SMTP, AWS SES, and Postmark backends so service code never depends on the chosen provider; ops can switch SMTP → SES with one config flag.
sender, err := email.New(cfg, email.WithLogger(svc.Logger()),
email.WithMetrics(svc.MetricsRegistry()))
err = sender.Send(ctx, email.Message{
From: email.Address{Email: "no-reply@app.io"},
To: []email.Address{{Email: "alice@example.com"}},
Subject: "Welcome",
HTMLBody: "<h1>Hi Alice!</h1>",
TextBody: "Hi Alice!",
})
Backends:
- Backend "smtp" — net/smtp via STARTTLS. Stdlib-only.
- Backend "ses" — AWS SES v2 (sesv2.SendEmail). Reuses aws-sdk-go-v2 config; honours IRSA / static creds.
- Backend "postmark" — Postmark HTTP API via clients/httpc. Uses X-Postmark-Server-Token auth.
- Backend "stub" — captures into Sent so tests can assert on outbound messages without a live transport.
Templates: optional html/template + text/template rendering via Templates.Render. Templates live as text files on disk; pass a glob or fs.FS to [LoadTemplates]. Kept lightweight — the kit is a transport, not a campaign tool.
Index ¶
Constants ¶
const ( // CodeInvalidConfig — New received an empty/unknown Backend, or // the backend's required fields were missing. CodeInvalidConfig = "email_invalid_config" // CodeInvalidMessage — Send rejected a Message because of // missing From / no recipients / empty Subject AND body. CodeInvalidMessage = "email_invalid_message" // CodeSendFailed — backend reported an error. The original // SDK / SMTP / HTTP error is wrapped in Cause. CodeSendFailed = "email_send_failed" // CodeTemplateNotFound — Templates.Render was called with an // unknown template name. CodeTemplateNotFound = "email_template_not_found" // CodeTemplateExecFailed — html/text template Execute returned // an error. Usually a missing field on the supplied data. CodeTemplateExecFailed = "email_template_exec_failed" )
Stable error Code constants produced by the package.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Address ¶
Address is one mailbox. Name is optional (used in From-header formatting); Email is required.
type Attachment ¶
type Attachment struct {
Filename string
ContentType string // application/pdf, image/png, …
Data io.Reader
}
Attachment is one file attached to a Message. Data is read once at Send time (the io.Reader is fully consumed) — Reader implementations that don't support replay can't be reused across sends.
type Config ¶
type Config struct {
Backend string `env:"BACKEND"` // smtp|ses|postmark|stub
SMTP SMTPConfig
SES SESConfig
Postmark PostmarkConfig
}
Config picks the backend + carries backend-specific knobs. Exactly one of SMTP / SES / Postmark must be set when Backend is the matching value.
type Message ¶
type Message struct {
From Address
To []Address
CC []Address
BCC []Address
ReplyTo []Address
Subject string
HTMLBody string
TextBody string
Headers map[string]string
Attachments []Attachment
// Tag is an opt-in tracking label propagated to backends that
// support it (Postmark X-PM-Message-Stream / SES Tags). Maps
// to nothing on plain SMTP.
Tag string
}
Message is the wire-shape Sender.Send consumes. The minimal valid shape is From + (≥1 of To/CC/BCC) + Subject + (HTMLBody OR TextBody). Validate enforces the contract.
func (Message) AllRecipients ¶
AllRecipients returns the flat To+CC+BCC slice. Used by SMTP backends that need the envelope-RCPT-TO list.
func (Message) Validate ¶
Validate reports the message-level contract:
- From.Email non-empty
- at least one recipient across To/CC/BCC
- Subject non-empty
- HTMLBody OR TextBody non-empty
Returns *errs.Error{Code: CodeInvalidMessage} on failure.
type Option ¶
type Option func(*options)
Option tunes New.
func WithLogger ¶
WithLogger installs a *slog.Logger that receives Debug on success and Warn on send failures. nil silences output.
func WithMetrics ¶
func WithMetrics(reg prometheus.Registerer) Option
WithMetrics registers Prometheus collectors:
- email_send_total{backend, outcome}
- email_send_duration_seconds{backend}
nil reg no-ops.
type PostmarkConfig ¶
type PostmarkConfig struct {
// ServerToken is the per-server token. Found under Postmark
// → Server → API Tokens.
ServerToken string `env:"SERVER_TOKEN"`
// MessageStream picks the stream (default "outbound"). Postmark
// requires a matching stream for broadcasts vs transactional.
MessageStream string `env:"MESSAGE_STREAM" envDefault:"outbound"`
// Endpoint overrides the default https://api.postmarkapp.com.
// Used by tests + region-pinned EU deployments
// (https://api.eu.postmarkapp.com).
Endpoint string `env:"ENDPOINT"`
// Timeout caps the round-trip per Send. Default 15s.
Timeout time.Duration `env:"TIMEOUT" envDefault:"15s"`
// HTTPClient is an optional override. Set to wire kit's
// clients/httpc retries / metrics. nil falls back to a
// fresh net/http.DefaultClient with Timeout.
HTTPClient *http.Client
}
PostmarkConfig is the backend-specific block for Config when Backend == "postmark". ServerToken is required.
type SESConfig ¶
type SESConfig struct {
Region string `env:"REGION" envDefault:"us-east-1"`
AccessKeyID string `env:"ACCESS_KEY_ID"`
SecretAccessKey string `env:"SECRET_ACCESS_KEY"`
SessionToken string `env:"SESSION_TOKEN"`
// ConfigurationSet activates per-set SES features (event
// publishing to SNS, IP-pool routing). Empty leaves the default.
ConfigurationSet string `env:"CONFIGURATION_SET"`
// Endpoint overrides the default service endpoint (LocalStack /
// kit tests). Empty uses AWS resolved endpoint.
Endpoint string `env:"ENDPOINT"`
// Timeout caps the round-trip per Send. Default 15s.
Timeout time.Duration `env:"TIMEOUT" envDefault:"15s"`
}
SESConfig is the backend-specific block for Config when Backend == "ses". All fields are optional — empty AccessKeyID + SecretAccessKey falls back to the SDK default credential chain (env, instance-profile, IRSA).
type SMTPConfig ¶
type SMTPConfig struct {
Host string `env:"HOST"`
Port int `env:"PORT" envDefault:"587"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
// AuthMethod is the credential mechanism used. "plain" (default)
// runs PLAIN over STARTTLS; "cram-md5" uses CRAM-MD5. Empty
// Username disables auth entirely (no-auth relay).
AuthMethod string `env:"AUTH_METHOD"`
// LocalName is the HELO/EHLO domain. Empty defaults to
// "localhost"; set to your sending host for IP-based reputation
// systems that match HELO against rDNS.
LocalName string `env:"LOCAL_NAME"`
// StartTLS toggles STARTTLS upgrade. true (default) is correct
// for port 587; set false ONLY for port 25 to local mail-relay
// inside trusted networks.
StartTLS bool `env:"START_TLS" envDefault:"true"`
// Timeout caps the total round-trip per Send. Default 30s.
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
SMTPConfig is the backend-specific block for Config when Backend == "smtp". Host + Port are required.
type Sender ¶
Sender is the abstract contract every backend satisfies. Goroutine-safe — implementations protect their own state.
type Stub ¶
type Stub struct {
// contains filtered or unexported fields
}
Stub is an in-memory Sender for tests / dev / staging where real delivery is undesirable. Captures every successful Send into Sent so test code can assert. Goroutine-safe.
type Templates ¶
type Templates struct {
// contains filtered or unexported fields
}
Templates is a thread-safe template registry. Stores parsed html/template + text/template pairs keyed by base name. Use Templates.Render to fill a Message's HTMLBody / TextBody from data.
Convention: a template named "welcome" loads from "welcome.html.tmpl" + "welcome.txt.tmpl" (either OR both). Missing halves render to empty body; Send still validates that at least one body part is non-empty.
func NewTemplates ¶
func NewTemplates() *Templates
NewTemplates returns an empty registry. Populate via [LoadGlob] or [LoadFS].
func (*Templates) LoadGlob ¶
LoadGlob loads every `*.html.tmpl` and `*.txt.tmpl` matched by pat (forwarded to filepath.Glob). Base name is the file stem with the `.html` / `.txt` suffix stripped, e.g. `welcome.html.tmpl` → `welcome`. Subsequent loads for the same name overwrite the prior parse (test-friendly).
func (*Templates) Render ¶
Render fills msg.HTMLBody and msg.TextBody from the templates registered under name. Either side may be empty if the matching `*.html.tmpl` / `*.txt.tmpl` wasn't loaded — Send's Validate still requires at least one populated body, so callers should ship at least one half per template.
Returns *errs.Error{Code: CodeTemplateNotFound} when neither the html- nor text-half is registered.