email

package
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

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

View Source
const (
	CodeSPFTooManyLookups = "spf_too_many_lookups"
	CodeDKIMProxied       = "dkim_cname_proxied"
	CodeNotAligned        = "from_not_aligned"
	CodeAligned           = "from_aligned"

	RecordAlignment = "alignment"
)

Diagnosis codes added on top of the base SPF, DKIM and DMARC codes.

View Source
const (
	RecordSPF   = "spf"
	RecordDKIM  = "dkim"
	RecordDMARC = "dmarc"

	CheckPass = "pass"
	CheckWarn = "warn"
)

Domain check record types and statuses.

View Source
const (
	DeliverySent    = "sent"
	DeliveryFailed  = "failed"
	DeliveryBounced = "bounced"
)

Delivery statuses.

View Source
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.

View Source
const (
	DefaultFailoverThreshold = 3
	DefaultFailoverCooldown  = 60 * time.Second
)

Defaults for the circuit breaker; the boot code overrides them from env.

View Source
const (
	MailgunRegionUS = "us"
	MailgunRegionEU = "eu"
)

Mailgun regions an operator can pick.

Variables

View Source
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.

View Source
var ErrPinMismatch = errors.New("email: server certificate does not match the pinned fingerprint")

ErrPinMismatch is a certificate that does not match the stored pin.

View Source
var ErrRateLimited = errors.New("email: send rate limit reached")

ErrRateLimited is returned when the configured sends-per-minute or per-day cap is reached.

View Source
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 CertSHA256

func CertSHA256(der []byte) string

CertSHA256 is the pin form of a certificate: hex SHA-256 of its DER bytes.

func CountSPFLookups

func CountSPFLookups(ctx context.Context, dns DNSLookup, record string, maxDepth int) int

CountSPFLookups counts the DNS-querying terms of an SPF record and of every record it includes or redirects to. It stops descending at maxDepth and reports the running total, so a loop or a deep chain cannot run away.

func DKIMProxied

func DKIMProxied(ctx context.Context, dns DNSLookup, name string) bool

DKIMProxied reports whether name answers with Cloudflare addresses instead of the CNAME a DKIM delegation needs. It returns false when the resolver cannot look up addresses, so it never guesses.

func DomainOf

func DomainOf(from string) string

DomainOf returns the domain part of an address ("Name <a@b.com>" or "a@b.com").

func FailoverEligible

func FailoverEligible(err error) bool

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 FetchSMTPCertSHA256

func FetchSMTPCertSHA256(ctx context.Context, addr string) (string, error)

FetchSMTPCertSHA256 reads the leaf certificate a submission server presents (implicit TLS on 465, STARTTLS otherwise). It authenticates nothing; callers use it only on a private network they trust.

func FormatFrom

func FormatFrom(name, addr string) string

FormatFrom renders "Name <addr>" (or the bare address when name is empty).

func FormatPin

func FormatPin(addr, sha string) string

FormatPin binds a pin to one address so it never applies to another server.

func IsTransient

func IsTransient(err error) bool

IsTransient reports whether a retry could plausibly succeed.

func OnlyProviderID

func OnlyProviderID(ctx context.Context) string

OnlyProviderID returns the id set by WithOnlyProvider, or "".

func PinFor

func PinFor(stored, addr string) string

PinFor returns the pin stored for addr, or "" when the stored pin names another address.

func RedactProviderText

func RedactProviderText(s string, secrets ...string) string

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

func Register(b Backend, fn Builder)

Register adds a provider to the registry; each provider file calls it from init, so adding one is one file.

func SESSMTPHost

func SESSMTPHost(region string) (string, error)

SESSMTPHost derives the SES SMTP endpoint for region.

func SendMessage

func SendMessage(ctx context.Context, s Sender, m Message) error

SendMessage delivers m through s. A Sender without HTML support gets the plain-text part through Send, so mail never silently disappears.

func SendWithID

func SendWithID(ctx context.Context, s Sender, to, subject, body string) (string, error)

SendWithID sends through s and returns the provider message id when the sender exposes one.

func ValidBackend

func ValidBackend(b Backend) bool

ValidBackend reports whether b is a registered provider.

func ValidSESRegion

func ValidSESRegion(region string) bool

ValidSESRegion reports whether region is a listed SES region.

func ValidateAddress added in v0.2.0

func ValidateAddress(s string) error

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

func WithKind(ctx context.Context, kind string) context.Context

WithKind tags sends made with ctx (for example "password_reset") in the delivery log.

func WithOnlyProvider

func WithOnlyProvider(ctx context.Context, id string) context.Context

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

func WithoutFailover(ctx context.Context) context.Context

WithoutFailover keeps sends made with ctx on the primary provider even when it fails.

func WithoutRetry

func WithoutRetry(ctx context.Context) context.Context

WithoutRetry disables retries for sends made with ctx, for interactive test sends. It also disables failover: a test send must report on the provider it was aimed at.

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.

func Backends

func Backends() []Backend

Backends lists every registered provider in a stable order.

type Builder

type Builder func(cfg Config) (Sender, error)

Builder turns a Config into the Sender for one provider.

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

type ConfigLoader func(ctx context.Context) (Config, error)

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

func CheckDomain(ctx context.Context, dns DNSLookup, domain string, provider Backend) DomainCheck

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

func (d *DynamicSender) SendDetailed(ctx context.Context, to, subject, body string) (Result, error)

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

func (d *DynamicSender) SendMessageDetailed(ctx context.Context, m Message) (Result, error)

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.

func Classify

func Classify(err error) FailureKind

Classify maps a send error to a FailureKind.

type HTTPError

type HTTPError struct {
	Provider string
	Status   int
	Body     string
}

HTTPError is a non-2xx answer from an HTTP provider API.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type HostLookup

type HostLookup interface {
	LookupHost(ctx context.Context, host string) ([]string, error)
}

HostLookup is the optional address lookup some resolvers offer; the proxy check needs it.

type Limits

type Limits struct {
	PerMinute int
	PerDay    int
}

Limits caps how many sends the control plane will make; zero means unlimited.

type Message

type Message struct {
	To      string
	Subject string
	Text    string
	HTML    string
	Headers map[string]string
}

Message is an email with an HTML alternative and extra headers such as List-Unsubscribe.

type ProviderConfig

type ProviderConfig struct {
	ID     string
	Config Config
}

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.

func CheckAlignment

func CheckAlignment(from, spfDomain, dkimDomain string) RecordCheck

CheckAlignment grades DMARC alignment of the From domain against the SPF (return-path) and DKIM (d=) domains. Alignment is relaxed: the domains match, or one is a subdomain of the other.

func CheckDMARC

func CheckDMARC(ctx context.Context, dns DNSLookup, domain string) RecordCheck

CheckDMARC is the DMARC verdict alone.

func CheckSPF

func CheckSPF(ctx context.Context, dns DNSLookup, domain, include string) RecordCheck

CheckSPF is the SPF verdict alone, for callers that grade records one by one.

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

type ResendConfig struct {
	APIKey  string
	From    string
	ReplyTo string
}

ResendConfig is a Resend sender's connection details.

type Result

type Result struct {
	Provider  string
	MessageID string
	Attempts  int
}

Result describes a successful send.

type RichSender

type RichSender interface {
	SendMessage(ctx context.Context, m Message) error
}

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
	// PinSHA256 trusts exactly one leaf certificate (hex SHA-256 of its DER) instead of the CA chain.
	PinSHA256 string
}

SMTPConfig is an SMTP server's connection details.

type SPFLimits

type SPFLimits struct {
	Lookups int
	Depth   int
}

SPFLimits bounds the lookup count and the include nesting walked by CountSPFLookups.

func SPFLimitsFromEnv

func SPFLimitsFromEnv() SPFLimits

SPFLimitsFromEnv reads APP_SPF_LOOKUP_LIMIT and APP_SPF_DEPTH_LIMIT. The defaults are the protocol limit of 10.

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.

func (*SendError) Error

func (e *SendError) Error() string

func (*SendError) Unwrap

func (e *SendError) Unwrap() error

type Sender

type Sender interface {
	Send(ctx context.Context, to, subject, body string) error
}

Sender sends one plain-text email.

func NewSender

func NewSender(cfg Config) (Sender, error)

NewSender builds the Sender cfg.Backend names, or ErrNotConfigured for an empty or unrecognized Backend.

type SystemDNS

type SystemDNS struct{}

SystemDNS is the control plane's own resolver, so the check needs no external service.

func (SystemDNS) LookupCNAME

func (SystemDNS) LookupCNAME(ctx context.Context, name string) (string, error)

LookupCNAME resolves a CNAME through the system resolver.

func (SystemDNS) LookupTXT

func (SystemDNS) LookupTXT(ctx context.Context, name string) ([]string, error)

LookupTXT resolves TXT records through the system resolver.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL