Documentation
¶
Overview ¶
Package email is the platform's one email-sending capability: a narrow Sender interface plus a registry of providers (SMTP, SES, Resend, Postmark, SendGrid, Mailgun), shared by internal/alerting and internal/api so neither imports the other.
Index ¶
- Constants
- Variables
- func DomainOf(from string) string
- func FailoverEligible(err error) bool
- func FormatFrom(name, addr string) string
- func IsTransient(err error) bool
- func OnlyProviderID(ctx context.Context) string
- func RedactProviderText(s string, secrets ...string) string
- func Register(b Backend, fn Builder)
- func SESSMTPHost(region string) (string, error)
- func SendMessage(ctx context.Context, s Sender, m Message) error
- func SendWithID(ctx context.Context, s Sender, to, subject, body string) (string, error)
- func ValidBackend(b Backend) bool
- func ValidSESRegion(region string) bool
- func ValidateAddress(s string) error
- func WithKind(ctx context.Context, kind string) context.Context
- func WithOnlyProvider(ctx context.Context, id string) context.Context
- func WithoutFailover(ctx context.Context) context.Context
- func WithoutRetry(ctx context.Context) context.Context
- type APIConfig
- type Backend
- type Builder
- type Config
- type ConfigLoader
- type DNSLookup
- type Delivery
- type DomainCheck
- type DynamicSender
- func (d *DynamicSender) Configured(ctx context.Context) bool
- func (d *DynamicSender) Send(ctx context.Context, to, subject, body string) error
- func (d *DynamicSender) SendDetailed(ctx context.Context, to, subject, body string) (Result, error)
- func (d *DynamicSender) SendMessage(ctx context.Context, m Message) error
- func (d *DynamicSender) SendMessageDetailed(ctx context.Context, m Message) (Result, error)
- func (d *DynamicSender) WithFailover(threshold int, cooldown time.Duration) *DynamicSender
- func (d *DynamicSender) WithLogger(l *slog.Logger) *DynamicSender
- func (d *DynamicSender) WithProviderHealth(h ProviderHealth) *DynamicSender
- func (d *DynamicSender) WithProviders(load ProvidersLoader) *DynamicSender
- func (d *DynamicSender) WithRecorder(r Recorder) *DynamicSender
- func (d *DynamicSender) WithRetry(attempts int, base time.Duration) *DynamicSender
- type FailureKind
- type HTTPError
- type Limits
- type Message
- type ProviderConfig
- type ProviderHealth
- type ProvidersLoader
- type RecordCheck
- type Recorder
- type ResendConfig
- type Result
- type RichSender
- type SESConfig
- type SMTPConfig
- type SendError
- type Sender
- type SystemDNS
Constants ¶
const ( RecordSPF = "spf" RecordDKIM = "dkim" RecordDMARC = "dmarc" CheckPass = "pass" CheckWarn = "warn" )
Domain check record types and statuses.
const ( DeliverySent = "sent" DeliveryFailed = "failed" DeliveryBounced = "bounced" )
Delivery statuses.
const ( TLSAuto = "" // STARTTLS when the server offers it, implicit TLS on port 465 TLSStartTLS = "starttls" // require STARTTLS TLSImplicit = "tls" // TLS from the first byte TLSNone = "none" // plaintext )
TLS modes for SMTP-based backends.
const ( DefaultFailoverThreshold = 3 DefaultFailoverCooldown = 60 * time.Second )
Defaults for the circuit breaker; the boot code overrides them from env.
const ( MailgunRegionUS = "us" MailgunRegionEU = "eu" )
Mailgun regions an operator can pick.
Variables ¶
var ErrNotConfigured = errors.New("email: not configured")
ErrNotConfigured is returned by NewSender when a Config names no backend: no settings row and no env-var fallback.
var ErrRateLimited = errors.New("email: send rate limit reached")
ErrRateLimited is returned when the configured sends-per-minute or per-day cap is reached.
var SESRegions = []string{
"us-east-1", "us-east-2", "us-west-1", "us-west-2",
"af-south-1",
"ap-south-1", "ap-south-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3",
"ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-5",
"ca-central-1", "ca-west-1",
"eu-central-1", "eu-central-2", "eu-west-1", "eu-west-2", "eu-west-3",
"eu-north-1", "eu-south-1", "eu-south-2",
"il-central-1", "me-south-1", "me-central-1", "mx-central-1", "sa-east-1",
"us-gov-west-1",
}
SESRegions is the one registry of AWS regions that offer SES; the API validates against it and the UI mirrors it.
Functions ¶
func FailoverEligible ¶
FailoverEligible reports whether err is a pre-send provider failure that justifies trying the backup. When it is unclear whether the body reached the provider it returns false, so mail is never duplicated.
func FormatFrom ¶
FormatFrom renders "Name <addr>" (or the bare address when name is empty).
func IsTransient ¶
IsTransient reports whether a retry could plausibly succeed.
func OnlyProviderID ¶
OnlyProviderID returns the id set by WithOnlyProvider, or "".
func RedactProviderText ¶
RedactProviderText truncates provider output and strips credentials and recipient addresses from it. secrets are exact values (passwords, API keys) to remove wherever they appear.
func Register ¶
Register adds a provider to the registry; each provider file calls it from init, so adding one is one file.
func SESSMTPHost ¶
SESSMTPHost derives the SES SMTP endpoint for region.
func SendMessage ¶
SendMessage delivers m through s. A Sender without HTML support gets the plain-text part through Send, so mail never silently disappears.
func SendWithID ¶
SendWithID sends through s and returns the provider message id when the sender exposes one.
func ValidBackend ¶
ValidBackend reports whether b is a registered provider.
func ValidSESRegion ¶
ValidSESRegion reports whether region is a listed SES region.
func ValidateAddress ¶ added in v0.2.0
ValidateAddress reports an error unless s is a single bare email address ("a@b.com", not "Name <a@b.com>" or a list).
func WithKind ¶
WithKind tags sends made with ctx (for example "password_reset") in the delivery log.
func WithOnlyProvider ¶
WithOnlyProvider aims sends made with ctx at one provider id, with no failover. A ProvidersLoader should honor it even for a disabled provider, so a test send can prove a provider before it is enabled.
func WithoutFailover ¶
WithoutFailover keeps sends made with ctx on the primary provider even when it fails.
Types ¶
type APIConfig ¶
type APIConfig struct {
APIKey string
From string
ReplyTo string
// Domain and Region are Mailgun-only.
Domain string
Region string
}
APIConfig is the connection detail shared by the key-based HTTP providers (Postmark, SendGrid, Mailgun).
type Backend ¶
type Backend string
Backend names which transport a Config uses to actually send.
const ( BackendSMTP Backend = "smtp" BackendSES Backend = "ses" BackendSESSMTP Backend = "ses_smtp" BackendResend Backend = "resend" BackendPostmark Backend = "postmark" BackendSendGrid Backend = "sendgrid" BackendMailgun Backend = "mailgun" )
The built-in backends; each registers itself in its own file.
type Config ¶
type Config struct {
Backend Backend
SMTP *SMTPConfig
SES *SESConfig
Resend *ResendConfig
API *APIConfig
Limits Limits
}
Config names which backend to use plus that backend's own settings.
type ConfigLoader ¶
ConfigLoader resolves the current Config on every call, never cached: settings can change at runtime.
type DNSLookup ¶
type DNSLookup interface {
LookupTXT(ctx context.Context, name string) ([]string, error)
LookupCNAME(ctx context.Context, name string) (string, error)
}
DNSLookup is the slice of net.Resolver the domain check needs, so tests can fake it.
type Delivery ¶
type Delivery struct {
At time.Time
Provider string
To string
Kind string
Status string // DeliverySent or DeliveryFailed
MessageID string
Error string
ErrorKind FailureKind
Attempts int
// ProviderID is the provider row that handled the attempt; FailoverFrom is the one it replaced, if any.
ProviderID string
FailoverFrom string
}
Delivery is one send outcome handed to a Recorder.
type DomainCheck ¶
type DomainCheck struct {
Domain string `json:"domain"`
Provider string `json:"provider"`
Records []RecordCheck `json:"records"`
// SPFInclude is the provider's expected SPF include, shown as a hint.
SPFInclude string `json:"spf_include,omitempty"`
}
DomainCheck is the SPF, DKIM and DMARC verdict for a sending domain.
func CheckDomain ¶
CheckDomain looks up SPF, DKIM and DMARC for domain and compares them with what provider needs.
type DynamicSender ¶
type DynamicSender struct {
// contains filtered or unexported fields
}
DynamicSender adapts a ConfigLoader into a Sender, so a settings change takes effect on the very next send with no restart required.
func NewDynamicSender ¶
func NewDynamicSender(load ConfigLoader) *DynamicSender
NewDynamicSender builds a DynamicSender. load must not be nil.
func (*DynamicSender) Configured ¶
func (d *DynamicSender) Configured(ctx context.Context) bool
Configured reports whether the current settings name a backend that can send, so callers can tell "no mail server set up" from a failed send.
func (*DynamicSender) Send ¶
func (d *DynamicSender) Send(ctx context.Context, to, subject, body string) error
Send implements Sender. It rejects a CR or LF in to or subject before any backend runs, since either could start an injected header.
func (*DynamicSender) SendDetailed ¶
SendDetailed is Send plus the provider and message id of a successful send.
func (*DynamicSender) SendMessage ¶
func (d *DynamicSender) SendMessage(ctx context.Context, m Message) error
SendMessage implements RichSender through the current settings, with the same retries, caps and delivery log as Send.
func (*DynamicSender) SendMessageDetailed ¶
SendMessageDetailed is SendMessage plus the provider and message id of a successful send.
func (*DynamicSender) WithFailover ¶
func (d *DynamicSender) WithFailover(threshold int, cooldown time.Duration) *DynamicSender
WithFailover sets how many consecutive failover-class failures open a provider's circuit and how long it stays open.
func (*DynamicSender) WithLogger ¶
func (d *DynamicSender) WithLogger(l *slog.Logger) *DynamicSender
WithLogger enables structured failover logging; without it nothing is logged.
func (*DynamicSender) WithProviderHealth ¶
func (d *DynamicSender) WithProviderHealth(h ProviderHealth) *DynamicSender
WithProviderHealth persists last_ok and last error kind per provider after sends.
func (*DynamicSender) WithProviders ¶
func (d *DynamicSender) WithProviders(load ProvidersLoader) *DynamicSender
WithProviders makes sends use an ordered provider list (primary first, optional backup) instead of the single ConfigLoader.
func (*DynamicSender) WithRecorder ¶
func (d *DynamicSender) WithRecorder(r Recorder) *DynamicSender
WithRecorder logs every send outcome and enforces the configured send caps.
func (*DynamicSender) WithRetry ¶
func (d *DynamicSender) WithRetry(attempts int, base time.Duration) *DynamicSender
WithRetry sets how many attempts a transient provider failure gets and the first backoff delay.
type FailureKind ¶
type FailureKind string
FailureKind is the typed reason a send failed, mapped to a Fix in the UI.
const ( FailAuth FailureKind = "auth_failed" FailSenderNotVerified FailureKind = "sender_not_verified" FailRegionMismatch FailureKind = "region_mismatch" FailPortBlocked FailureKind = "port_blocked" FailTLS FailureKind = "tls_error" FailRateLimited FailureKind = "rate_limited" FailNotConfigured FailureKind = "not_configured" FailOther FailureKind = "other" )
The failure kinds Classify returns.
type Message ¶
Message is an email with an HTML alternative and extra headers such as List-Unsubscribe.
type ProviderConfig ¶
ProviderConfig is one configured sending provider: a stable id plus the Config built from its row and credentials. The id is empty for the env fallback and the legacy single-row path, which never fail over.
type ProviderHealth ¶
type ProviderHealth interface {
RecordProviderResult(ctx context.Context, providerID string, ok bool, kind FailureKind, at time.Time) error
}
ProviderHealth persists the last outcome per provider. Implementations are best effort.
type ProvidersLoader ¶
type ProvidersLoader func(ctx context.Context) ([]ProviderConfig, error)
ProvidersLoader resolves the ordered provider list on every send: primary first, then the enabled backup.
type RecordCheck ¶
type RecordCheck struct {
Type string `json:"type"`
Name string `json:"name"`
Status string `json:"status"`
Code string `json:"code"`
Value string `json:"value,omitempty"`
Message string `json:"message"`
}
RecordCheck is one DNS record's verdict. Code is a stable key the UI translates; Message is the English text the CLI prints.
type Recorder ¶
type Recorder interface {
RecordDelivery(ctx context.Context, d Delivery) error
CountSent(ctx context.Context, since time.Time) (int, error)
}
Recorder persists delivery outcomes and answers the sent-count questions the rate caps need.
type ResendConfig ¶ added in v0.2.0
ResendConfig is a Resend sender's connection details.
type RichSender ¶
RichSender is implemented by senders that can deliver Message with its HTML part and headers intact.
type SESConfig ¶
type SESConfig struct {
Region string
AccessKeyID string
SecretAccessKey string
From string
ReplyTo string
}
SESConfig is an AWS SES v2 sender's connection details.
type SMTPConfig ¶
type SMTPConfig struct {
// Addr is host:port, e.g. "smtp.example.com:587".
Addr string
Host string // just the host part of Addr, for PlainAuth's identity check
Username string
Password string
From string
// EnvelopeFrom is the bare MAIL FROM address; empty uses From.
EnvelopeFrom string
ReplyTo string
TLSMode string
SkipVerify bool
}
SMTPConfig is an SMTP server's connection details.
type SendError ¶
type SendError struct {
Kind FailureKind
Attempts int
Err error
}
SendError wraps a final failure with its kind and how many attempts were made.
type SystemDNS ¶
type SystemDNS struct{}
SystemDNS is the control plane's own resolver, so the check needs no external service.
func (SystemDNS) LookupCNAME ¶
LookupCNAME resolves a CNAME through the system resolver.