mail

package
v1.28.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Index

Constants

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

func Enqueue(ctx context.Context, store *outbox.Store, msg Message) (outbox.Message, error)

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

func SetPluginHost(host plugins.Host)

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

type HealthChecker interface {
	Healthy(ctx context.Context) error
}

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.

func (*OutboxBridge) Send added in v1.28.0

func (b *OutboxBridge) Send(ctx context.Context, msg outbox.Message) error

Send implements outbox.Bridge: it decodes the queued message and hands it to the sender. A payload that does not decode is returned as an error, so it lands in the outbox's failed state with its reason instead of being dropped.

type ProviderFactory

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

ProviderFactory builds a Sender from provider-specific configuration.

type Sender

type Sender interface {
	Send(ctx context.Context, message Message) error
}

Sender sends outbound email messages.

func NewSender

func NewSender(cfg Config) (Sender, error)

NewSender resolves and constructs a mail sender for the given configuration.

Resolution order: 1) built-in or registered provider 2) executable plugin on PATH named nucleus-plugin-<driver> with capability mail.send

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

func ParseFS(fsys fs.FS, patterns ...string) (*Templates, error)

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

func (t *Templates) Names() []string

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

func (t *Templates) Render(name string, data any, base Message) (Message, error)

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.

Jump to

Keyboard shortcuts

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