mail

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package mail is the outbound adapter for the mail port: SMTP, and the health probe that reads the same breaker it trips.

The standard library's net/smtp is what carries it. It is frozen rather than maintained, which is precisely the argument for using it here: what this adapter needs is EHLO, STARTTLS, AUTH and DATA, none of which has changed since the package was frozen - and a third-party mailer would be a supply chain decision (security.md §11) bought for nothing.

Index

Constants

View Source
const Dependency = "smtp"

Dependency is the name this adapter appears under in the health report and in the metrics. Short and without spaces, as core/port/health requires.

View Source
const NotificationsFeature = "notifications"

NotificationsFeature is what a person cares about when the mail server is gone: being told things, not SMTP (observability-reliability.md §7). The bare feature name the degradation report carries.

Variables

This section is empty.

Functions

This section is empty.

Types

type Dial

type Dial func(ctx context.Context, network, address string) (net.Conn, error)

Dial opens the connection to the mail server. A field rather than a call to net.Dial, so that a test can put a scripted server on the other end of a real socket without the adapter knowing - the alternative is an SMTP adapter proved only by reading it.

type Probe

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

Probe reports the mail server's health to the registry by reading the breaker rather than dialling it - a probe that opened an SMTP session on every scrape would be a probe some servers rate-limit, and one that called a target the breaker has cut off would undo the breaker's whole purpose.

Optional by contract (core/port/health), and that is the point of the whole design: SMTP down restricts notifications and nothing else. The write path stays open, the comment that caused the notification is committed, and the message waits (observability-reliability.md §7).

func NewProbe

func NewProbe(breaker *resilience.Breaker, configured bool) Probe

NewProbe takes the breaker the resilient sender trips, and whether mail is configured.

func (Probe) Check

func (p Probe) Check(context.Context) health.Result

func (Probe) Name

func (p Probe) Name() string

func (Probe) Required

func (p Probe) Required() bool

type ResilientSender

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

ResilientSender composes a Sender with the resilience blocks (ADR-0016): a breaker so a dead mail server costs an immediate answer instead of a blocked worker for every queued message, and a bulkhead so the delivery of notifications cannot take more of the process than its share.

It matters more here than anywhere else that the breaker exists. A tenant with a busy morning has a queue full of notifications, and without a breaker each of them would spend the dial timeout finding out what the one before it already knew.

func NewResilientSender

func NewResilientSender(
	inner port.Sender, breaker *resilience.Breaker, bulkhead *resilience.Bulkhead,
) ResilientSender

NewResilientSender wraps the sender. One breaker and one bulkhead per dependency, owned by the composition root - the health probe reads the same breaker.

func (ResilientSender) Send

func (r ResilientSender) Send(ctx context.Context, message port.Message) error

Send runs the inner sender behind the bulkhead and the breaker, in that order: a call that cannot get a slot must not count against the breaker, because a full pool is this process's state and not the mail server's.

type SMTP

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

SMTP sends through a mail server.

One value for the whole process, holding no connection: a mail server is reached rarely enough that a pool would mostly be idle sockets a firewall eventually drops, and reconnecting per message is what makes an outage a failed attempt rather than a stuck one.

func NewSMTP

func NewSMTP(config env.MailConfig) SMTP

NewSMTP builds the sender from the installation's configuration.

func (SMTP) Send

func (s SMTP) Send(ctx context.Context, message port.Message) error

Send delivers one message.

Every step is inside the caller's deadline, or inside the configured timeout where the caller brought none (rule 7). The connection carries it as a socket deadline rather than as a cancellation watcher, because net/smtp has no context: a deadline on the connection is what actually stops a server that accepted the TCP handshake and then says nothing.

func (SMTP) WithDial

func (s SMTP) WithDial(dial Dial) SMTP

WithDial replaces the dialler. For the adapter's own tests, and for nothing else.

Jump to

Keyboard shortcuts

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