Skip to main content
Version: Next (unreleased)

Middleware

Middleware lets you weave cross-cutting concerns — logging, metrics, hooks, recovery, tracing — around any Sender without modifying it. Each middleware is a function with the signature func(Sender) Sender, so they compose cleanly.

Chain

email.Chain applies middleware in the order they appear — the first middleware is the outermost wrapper, executed first on the way in and last on the way out:

wrapped := email.Chain(sender,
email.WithRecovery(), // outermost: catches panics from anything below
email.WithLogging(logger), // logs send + duration
email.WithMetrics(metrics), // increments counters
email.WithHooks(hooks), // innermost: closest to actual send
)

mailer := email.NewMailer(wrapped, "no-reply@example.com")

Built-in Middleware

Four middleware ship with the core package. They cover the most common needs and are safe to stack in any order.

MiddlewareBehavior
WithLogging(Logger)Logs send start, success, and failure with duration
WithRecovery()Catches panics and returns ErrPanicked
WithHooks(SendHooks)OnSend, OnSuccess, OnFailure callbacks
WithMetrics(MetricsCollector)Counters and duration histograms via the metrics interface

Send Hooks

WithHooks is the easiest way to plug in audit logging, success counters, or alerting without writing a full middleware:

hooks := email.SendHooks{
OnSend: func(ctx context.Context, e *email.Email) {
log.Printf("sending to %v", e.To)
},
OnSuccess: func(ctx context.Context, e *email.Email, d time.Duration) {
metrics.SendDurationMS.Observe(d.Seconds() * 1000)
},
OnFailure: func(ctx context.Context, e *email.Email, err error) {
alerts.Notify("email failed", "to", e.To, "err", err)
},
}

wrapped := email.Chain(sender, email.WithHooks(hooks))

Custom Middleware

Implement email.Middleware directly when you need custom behavior — distributed tracing, throttling per recipient, blacklist filtering, etc.

type blacklistSender struct {
next email.Sender
deny map[string]bool
}

func (s *blacklistSender) Send(ctx context.Context, e *email.Email) error {
for _, to := range e.To {
if s.deny[strings.ToLower(to)] {
return fmt.Errorf("recipient %q is blacklisted", to)
}
}
return s.next.Send(ctx, e)
}

func (s *blacklistSender) Close() error { return s.next.Close() }

func WithBlacklist(deny map[string]bool) email.Middleware {
return func(next email.Sender) email.Sender {
return &blacklistSender{next: next, deny: deny}
}
}

wrapped := email.Chain(sender, WithBlacklist(myDenyList))