Documentation
¶
Index ¶
- Constants
- func Enqueue(ctx context.Context, store *outbox.Store, msg Message) (outbox.Message, error)
- func EnqueueTx(ctx context.Context, store *outbox.Store, tx *sql.Tx, msg Message) (outbox.Message, error)
- func RegisterProvider(name string, factory ProviderFactory) error
- func RegisteredProviders() []string
- func SetPluginHost(host plugins.Host)
- type Attachment
- type CircuitBreakerConfig
- type Config
- type HealthChecker
- type Message
- type OutboxBridge
- type ProviderFactory
- type Sender
- type Templates
Constants ¶
const OutboxTopic = "nucleus.mail"
OutboxTopic is the topic mail is queued under.
Variables ¶
This section is empty.
Functions ¶
func Enqueue ¶ added in v1.28.0
Enqueue queues a message outside a transaction. Prefer EnqueueTx whenever the mail accompanies a write: this one has the same window as sending directly, minus the retry.
func EnqueueTx ¶ added in v1.28.0
func EnqueueTx(ctx context.Context, store *outbox.Store, tx *sql.Tx, msg Message) (outbox.Message, error)
EnqueueTx queues a message for delivery INSIDE the caller's transaction, which is the only way a verification email and the row it announces can agree. Sending straight from a handler has a window that no retry closes: the database commits, the process dies, and the address is verified for an account whose owner never got the link — or the mail goes out and the commit rolls back, and the link points at nothing.
The message is delivered later by the dispatcher, through the bridge NewOutboxBridge returns, with the outbox's own retry and backoff.
func RegisterProvider ¶
func RegisterProvider(name string, factory ProviderFactory) error
RegisterProvider registers a named mail provider factory.
func RegisteredProviders ¶
func RegisteredProviders() []string
RegisteredProviders returns the currently registered provider names sorted alphabetically. Built-in providers are included.
func SetPluginHost ¶
SetPluginHost overrides the plugin runtime host used for external providers. Passing nil resets the default local executable host.
Types ¶
type Attachment ¶ added in v1.28.0
type Attachment struct {
// Filename is the name the recipient sees. It is sanitised on
// emission: a caller's filename ends up as a name on someone's disk
// and inside a header, so it is input, not decoration.
Filename string
// ContentType defaults to application/octet-stream.
ContentType string
// Content is the raw file, encoded base64 on emission.
Content []byte
// Inline marks an attachment meant to be rendered inside the HTML
// body (Content-Disposition: inline) rather than offered as a file.
Inline bool
// ContentID is the identifier the HTML refers to as cid:<ContentID>.
// It only means anything for an inline attachment.
ContentID string
}
Attachment is one file carried by a Message.
type CircuitBreakerConfig ¶
type CircuitBreakerConfig struct {
// Enabled turns on circuit-breaker wrapping around Send.
Enabled bool `koanf:"enabled"`
// FailureThreshold is the number of consecutive Send failures
// required to trip the breaker open. Non-positive falls back to
// pkg/circuit's default (1).
FailureThreshold int `koanf:"failure_threshold"`
// Cooldown is the duration the breaker stays open before admitting
// half-open probes. Non-positive falls back to pkg/circuit's
// default (30s).
Cooldown time.Duration `koanf:"cooldown"`
// HalfOpenMaxConcurrent caps in-flight probes in the half-open
// state. Non-positive falls back to pkg/circuit's default (1).
HalfOpenMaxConcurrent int `koanf:"half_open_max_concurrent"`
}
CircuitBreakerConfig configures the optional circuit breaker that wraps mail sender Send calls. Zero values are not used directly; pkg/app applies framework defaults before constructing the breaker.
The breaker is wrapped around Send only. Healthy (when the underlying sender implements HealthChecker) bypasses the breaker so /healthz can observe a recovering dependency even while Send is short-circuited.
type Config ¶
type Config struct {
Driver string
Timeout time.Duration
// SMTP
SMTPHost string
SMTPPort int
SMTPUser string
SMTPPass string
// CircuitBreaker, when Enabled, wraps the returned Sender.Send
// with a pkg/circuit breaker. Healthy (if the underlying provider
// implements HealthChecker) bypasses the breaker so /healthz
// observes a recovering dependency.
CircuitBreaker CircuitBreakerConfig
// Logger receives the circuit breaker's state-transition lines
// (NF-9: the breaker used to open and close in silence). Nil falls
// back to slog.Default().
Logger *slog.Logger
}
Config holds provider-agnostic and provider-specific mail settings. Only protocol-universal providers (SMTP) ship in-tree; provider- specific senders (SendGrid, Mailgun, AWS SES, Postmark, …) are installed as `nucleus-plugin-<provider>` binaries on PATH and discovered via the external sender. The `mail.send` capability contract is documented in `docs/reference/PLUGIN_SDK.md`.
type HealthChecker ¶
HealthChecker is an optional interface a Sender may implement to expose a non-destructive liveness check. The /healthz handler in pkg/app type-asserts for this interface; senders that do not implement it are not probed (so the response stays free of information-free "skipped" rows).
Implementations should keep Healthy cheap and non-destructive — at minimum, no actual mail is sent. For SMTP that means a TCP dial plus HELO/QUIT; for HTTP API providers it typically means a HEAD against a documented health endpoint.
type Message ¶
type Message struct {
From string
To []string
Subject string
Body string
// Headers holds optional custom headers appended after the
// framework-generated ones (From, To, Subject, MIME-Version,
// Content-Type). The built-in senders (SMTP and external
// plugins) validate the map on Send: a key must be non-empty
// and neither key nor value may contain CR or LF, so
// caller-supplied input cannot inject additional headers (e.g.
// an extra Bcc). Values are trimmed; a header whose value is
// empty after trimming is omitted. Custom providers registered
// via RegisterProvider are responsible for their own emission.
Headers map[string]string
// HTML is the alternative representation of Body. When it is set the
// message goes out as multipart/alternative with the plain text
// first, so a reader that cannot render HTML still gets the message
// rather than an encoded wall. Body is NOT optional when HTML is
// present: a message with no text alternative is what spam filters
// score against, and a text-only client would receive nothing.
HTML string
// Attachments are files carried with the message. Any attachment
// wraps the body in a multipart/mixed. An attachment whose Inline is
// set and whose ContentID is referenced from the HTML (cid:<id>) is
// rendered in place instead of listed.
Attachments []Attachment
}
Message represents one outbound email.
type OutboxBridge ¶ added in v1.28.0
type OutboxBridge struct {
// contains filtered or unexported fields
}
OutboxBridge delivers queued mail through a Sender. Register it on the outbox router for OutboxTopic and the dispatcher does the rest.
func NewOutboxBridge ¶ added in v1.28.0
func NewOutboxBridge(sender Sender) *OutboxBridge
NewOutboxBridge wraps a sender as an outbox bridge.
func (*OutboxBridge) Close ¶ added in v1.28.0
func (b *OutboxBridge) Close() error
Close implements outbox.Bridge. A sender owns no connection of its own — SMTP dials per message — so there is nothing to release.
func (*OutboxBridge) Healthy ¶ added in v1.28.0
func (b *OutboxBridge) Healthy(ctx context.Context) error
Healthy implements outbox.Bridge, forwarding to the sender when it knows how to answer.
func (*OutboxBridge) Name ¶ added in v1.28.0
func (b *OutboxBridge) Name() string
Name implements outbox.Bridge.
type ProviderFactory ¶
ProviderFactory builds a Sender from provider-specific configuration.
type Templates ¶ added in v1.28.0
type Templates struct {
// contains filtered or unexported fields
}
Templates renders messages from a set of named templates, so the wording of a verification or reset email lives in a file that a designer can edit rather than in a string literal inside a handler.
One name maps to up to three files, and the extension decides which engine parses them:
<name>.subject.tmpl required — text/template <name>.txt.tmpl required — text/template <name>.html.tmpl optional — html/template
The HTML half goes through html/template and NOT text/template, which is the whole reason the two engines are kept apart here: a username rendered into an HTML mail is untrusted input, and only html/template escapes it per context. The plain-text half must exist even when the HTML one does, because a message with no text alternative is what a text-only client and a spam filter both receive badly.
func ParseFS ¶ added in v1.28.0
ParseFS loads every .tmpl under the filesystem. An application embeds its own directory:
//go:embed mailtemplates var mailFS embed.FS t, err := mail.ParseFS(mailFS, "mailtemplates/*.tmpl")
func (*Templates) Names ¶ added in v1.28.0
Names returns the template names that can be rendered — the <name> of every <name>.txt.tmpl found.
func (*Templates) Render ¶ added in v1.28.0
Render builds a message from the named templates. base supplies everything the templates do not: From, To and any custom headers.
A subject that renders with a newline in it is REFUSED rather than trimmed: a newline in a subject is how a header injection starts, and silently repairing one hides the input that produced it.