notify

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package notify is a small unified-notifications primitive for GoFastr apps. Each notification is identified by a type ("order.shipped", "password.reset", etc.) and a Recipient; the Notifier renders a per-channel template and fans the rendered message out across every channel the routing function selects.

Channels are bundled separately so apps can wire whichever subset they need (email, in-app, webhook, push) without pulling in unrelated dependencies. A LoggerChannel ships in the package for development; the email channel adapter lives in this same package because battery/email is already a framework dependency.

Wiring:

tmpl := notify.NewMapTemplater()
tmpl.Set("order.shipped", "email", notify.Template{
    Subject:  "Your order has shipped",
    TextBody: "Hi {{name}}, your order #{{id}} is on its way.",
})
n := notify.New(
    notify.WithTemplater(tmpl),
    notify.WithChannel(notify.NewLoggerChannel(log.Default())),
)
err := n.Send(ctx, notify.Notification{
    Type: "order.shipped",
    To:   notify.Recipient{UserID: "u1", Email: "alice@example.com"},
    Data: map[string]any{"name": "Alice", "id": 42},
})

Index

Constants

View Source
const MaxInterpolatedOutputBytes = 1 << 20

MaxInterpolatedOutputBytes is the hard cap on the output of a single [interpolate] call. A 10 MB placeholder value would otherwise produce an unbounded subject / body string and pin a goroutine in fmt.Fprint. 1 MiB is well above any legitimate notification payload.

Variables

View Source
var (
	ErrNoTemplater = errors.New("notify: no templater installed")
	ErrNoChannels  = errors.New("notify: no channels selected for notification")
)

Errors returned by Send.

View Source
var ErrUnsafeHeader = errors.New("notify: unsafe header")

ErrUnsafeHeader is returned when rendered Extra contains a header name/value that would let a caller smuggle SMTP headers (CR/LF), or when it tries to override a reserved header that the channel owns.

View Source
var ErrUnsafeRecipient = errors.New("notify: unsafe recipient address")

ErrUnsafeRecipient is returned by EmailChannel.Send when the recipient address contains characters that would let a caller smuggle extra headers (CR / LF / NUL) or that fails a sanity-check on shape (must contain "@" and no embedded HTML / control bytes).

Functions

func DefaultRouter

func DefaultRouter(_ string, to Recipient, channels []string) []string

DefaultRouter selects channels by Recipient field presence:

  • "email" when To.Email != ""
  • "sms" when To.Phone != ""
  • "webhook" when To.Webhook != ""
  • "push" when len(To.PushTokens) > 0
  • "log" / "inapp" always

Channels with names not in this list are skipped — register a custom router via WithRouter to support more.

Types

type Channel

type Channel interface {
	Name() string
	Send(ctx context.Context, n Notification, r Rendered) error
}

Channel is the send-side adapter. Implementations must be safe for concurrent use; the Notifier may invoke Send from many goroutines.

type EmailChannel

type EmailChannel struct {
	// contains filtered or unexported fields
}

EmailChannel adapts a battery/email Sender to the notify Channel interface. Renders Subject/TextBody/HTMLBody from the Templater into the email's matching fields.

func NewEmailChannel

func NewEmailChannel(sender email.Sender, from string, opts ...EmailChannelOption) *EmailChannel

NewEmailChannel wraps a battery/email Sender with a from-address default. Pass any [EmailChannelOption]s to tweak the registration.

func (*EmailChannel) Name

func (c *EmailChannel) Name() string

Name implements Channel.

func (*EmailChannel) Send

Send implements Channel. Maps the Rendered payload onto an email.Email and dispatches via the wrapped Sender.

Recipient and from addresses are checked for the three shapes that turn an address into a header-injection vector: CR, LF, NUL. They are also screened for an "@" and rejected if they contain HTML tag characters — a literal "<script>alert(1)</script>@evil.com" is never a legitimate address. The downstream SMTP sender re-checks at the transport boundary, but rejecting early surfaces the bug nearer the data source.

type EmailChannelOption

type EmailChannelOption func(*EmailChannel)

EmailChannelOption configures the email adapter.

func WithEmailChannelName

func WithEmailChannelName(name string) EmailChannelOption

WithEmailChannelName overrides the registered name (default "email"). Useful when you want multiple email adapters (e.g. transactional vs marketing) on the same Notifier.

type LoggerChannel

type LoggerChannel struct {
	// contains filtered or unexported fields
}

LoggerChannel writes notifications to a *log.Logger — useful for development and CI. Always applies (DefaultRouter routes "log" or "inapp" unconditionally).

func NewLoggerChannel

func NewLoggerChannel(l *log.Logger) *LoggerChannel

NewLoggerChannel constructs a logger-backed channel. Pass nil to use the default logger; pass "" name for "log".

func (*LoggerChannel) Name

func (c *LoggerChannel) Name() string

Name returns "log".

func (*LoggerChannel) Send

Send writes a one-line record to the logger.

type MapTemplater

type MapTemplater struct {
	// contains filtered or unexported fields
}

MapTemplater is the simplest Templater: a (notifType, channel) → Template lookup table. Suitable for apps with a small fixed set of notifications; for catalog-driven i18n use [I18nTemplater].

func NewMapTemplater

func NewMapTemplater() *MapTemplater

NewMapTemplater returns an empty MapTemplater.

func (*MapTemplater) Render

func (m *MapTemplater) Render(_ context.Context, notifType, channel string, data map[string]any) (Rendered, error)

Render implements Templater.

The rendered Subject is stripped of CR / LF / NUL so a user-controlled {{placeholder}} can't inject header continuations when downstream transports (SMTP, push providers) treat Subject as a header value. TextBody / HTMLBody are not modified — those are payload bytes and any HTML safety is the rendering layer's job.

func (*MapTemplater) Set

func (m *MapTemplater) Set(notifType, channel string, t Template)

Set registers a template for the (notifType, channel) pair.

type Notification

type Notification struct {
	Type string
	To   Recipient
	Data map[string]any
}

Notification is one event-shaped message bound for a single Recipient. The Notifier picks channels by inspecting the Recipient (e.g. only sends to "email" channel when To.Email is non-empty), renders a per-channel template against Data, and fans out.

type Notifier

type Notifier struct {
	// contains filtered or unexported fields
}

Notifier is the front door. Construct with New, register channels via WithChannel, and call [Send] from handlers.

func New

func New(opts ...Option) *Notifier

New constructs a Notifier with the supplied options.

func (*Notifier) Channels

func (n *Notifier) Channels() []string

Channels returns the registered channel names in registration order.

func (*Notifier) Send

func (n *Notifier) Send(ctx context.Context, msg Notification) error

Send routes the notification, renders it per channel, and fires each channel concurrently. Returns the first error encountered (or nil); use WithErrorCallback to observe per-channel failures.

Notifications with no selected channel return ErrNoChannels. Notifications without a templater (and no pre-rendered payload in Data["_rendered"]) return ErrNoTemplater.

type Option

type Option func(*Notifier)

Option configures the Notifier.

func WithChannel

func WithChannel(c Channel) Option

WithChannel registers a Channel. Multiple calls accumulate; channel names must be unique (a second registration replaces the first).

func WithErrorCallback

func WithErrorCallback(fn func(channel string, n Notification, err error)) Option

WithErrorCallback installs a callback for per-channel send errors. The Notifier still attempts every selected channel; the callback surfaces failures so they can be logged/metricized without bringing the call site down.

func WithRouter

func WithRouter(r Router) Option

WithRouter installs a custom router. The default router uses DefaultRouter which selects channels by Recipient field presence.

func WithTemplater

func WithTemplater(t Templater) Option

WithTemplater installs a Templater. If unset, Notifier.Send returns ErrNoTemplater for any notification that doesn't carry a pre-rendered payload in Data["_rendered"].

type Recipient

type Recipient struct {
	UserID  string
	Email   string
	Phone   string
	Webhook string
	// PushTokens is the optional list of device tokens for push
	// channels — kept as a slice rather than a single field because
	// users frequently have multiple devices.
	PushTokens []string
}

Recipient is the destination set. Channels are responsible for reading the fields they care about — UserID is canonical, the rest are channel-specific addresses.

type Rendered

type Rendered struct {
	Subject  string
	TextBody string
	HTMLBody string
	// Extra carries channel-specific extras the templater wants the
	// channel to see — e.g. an attachment list, a SMS short-link, a
	// webhook payload override.
	Extra map[string]any
}

Rendered is the per-channel materialised message.

type Router

type Router func(notifType string, to Recipient, channels []string) []string

Router decides which channels a Notification should target. Default router: every registered channel that "applies" to the Recipient. Channels declare their applicability by name; for built-ins:

  • "email" applies when Recipient.Email != ""
  • "sms" applies when Recipient.Phone != ""
  • "webhook" applies when Recipient.Webhook != ""
  • "push" applies when len(Recipient.PushTokens) > 0
  • "log" / "inapp" always apply

type Template

type Template struct {
	Subject  string
	TextBody string
	HTMLBody string
	Extra    map[string]any
}

Template is one per-channel template. The fields are interpolated with the same `{{placeholder}}` form used by the i18n package.

type Templater

type Templater interface {
	Render(ctx context.Context, notifType, channel string, data map[string]any) (Rendered, error)
}

Templater renders a Notification + channel name into a Rendered payload. Implementations should be deterministic over the (notifType, channel, data) inputs — caching is the caller's choice.

Jump to

Keyboard shortcuts

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