outbound

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package outbound is the delivery queue: every message lfr sends is a row that survives restarts, is built and DKIM-signed at send time, handed to a relay or straight to the recipient's MX, and retried with backoff until it is delivered, refused for good or five days old.

Index

Constants

View Source
const (
	KindIssue   = "issue"   // one recipient of a newsletter issue
	KindConfirm = "confirm" // a double opt-in confirmation request
	KindMail    = "mail"    // a message sent from a mailbox (its raw content is a blob)
)

Kinds of outbound rows.

Variables

View Source
var ErrSkip = errors.New("skip")

ErrSkip cancels a row instead of sending it.

Functions

func Backoff

func Backoff(attempts int) time.Duration

Backoff is the wait before attempt n+1.

func IsHardBounce

func IsHardBounce(code int, enhanced, message string) bool

IsHardBounce decides whether a permanent failure means the address is gone (stop sending to it) rather than a policy or content rejection.

func Probe

func Probe(ctx context.Context, host string) error

Probe checks that outbound port 25 is reachable, the classic VPS block.

Types

type Builder

type Builder func(ctx context.Context, r Row) ([]byte, error)

Builder renders the unsigned message for a row. Returning an error wrapping ErrSkip cancels the row (e.g. the recipient unsubscribed meanwhile).

type Direct

type Direct struct {
	LocalName string
	DNS       dnsx.Resolver
	STS       *STSCache
	Port      string // "25"; tests use another
	Dial      func(ctx context.Context, network, addr string) (net.Conn, error)
	// IPv6 allows connections over IPv6. Off unless the server's IPv6 is
	// configured and has its own PTR: Gmail refuses IPv6 senders without
	// reverse DNS (550 5.7.25), and SPF covers only published addresses.
	IPv6 bool
}

Direct delivers to each recipient domain's MX on port 25, using TLS when offered and required when the domain publishes an enforcing MTA-STS policy.

func (*Direct) Close

func (d *Direct) Close()

Close is a no-op; direct connections are not pooled.

func (*Direct) Name

func (d *Direct) Name() string

Name describes the transport.

func (*Direct) Send

func (d *Direct) Send(ctx context.Context, from, rcpt string, raw []byte) error

Send delivers one message to rcpt's mail servers, trying each in preference order.

type Entry

type Entry struct {
	ID       int64     `json:"id"`
	Kind     string    `json:"kind"`
	Rcpt     string    `json:"rcpt"`
	Status   string    `json:"status"`
	Attempts int       `json:"attempts"`
	Next     time.Time `json:"next"`
	Error    string    `json:"error,omitempty"`
	Created  time.Time `json:"created"`
}

Entry is one queue row as listed.

type Execer

type Execer interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

Execer is a transaction or database that runs statements.

type Failure

type Failure struct {
	Temporary bool   // try again later
	Transport bool   // the relay itself is unreachable or refuses our login: nothing can go out
	Bounce    bool   // the address does not exist: suppress it
	Code      int    // SMTP reply code, 0 for network errors
	Enhanced  string // e.g. 5.1.1
	Message   string
	Handshake bool // refused before MAIL FROM: another MX may still accept
}

Failure is a delivery error with its consequence.

func Classify

func Classify(err error) *Failure

Classify turns any send error into a Failure.

func (*Failure) Error

func (f *Failure) Error() string

type Hooks

type Hooks struct {
	// Failed runs when delivery failed for good; f.Bounce says the address
	// does not exist.
	Failed func(ctx context.Context, r Row, f *Failure)
	// GaveUp runs when temporary failures outlasted MaxAge: a soft bounce.
	GaveUp func(ctx context.Context, r Row, reason string)
	Sent   func(ctx context.Context, r Row)
}

Hooks react to final outcomes, e.g. to suppress a bounced address.

type Item

type Item struct {
	Kind      string
	DomainID  int64
	IssueID   int64
	ContactID int64
	MemberID  int64
	Blob      string
	EnvFrom   string // empty: a VERP bounce address on the signing domain
	Rcpt      string
	At        time.Time // earliest send time; zero for now
}

Item is a message to queue.

type Queue

type Queue struct {
	DB        *store.DB
	Domains   *domains.Service
	Events    *events.Bus
	Builders  map[string]Builder
	Hooks     Hooks
	Bounce    func(r Row, domain string) string                              // VERP envelope sender
	Transport func(ctx context.Context) (t Transport, key string, err error) // key changes when the configuration does
	Rate      func(ctx context.Context) (perSecond, conns int)
	Now       func() time.Time
	Log       *slog.Logger
	MaxAge    time.Duration // give up after this long; default 5 days
	// contains filtered or unexported fields
}

Queue runs deliveries.

func (*Queue) Drop

func (q *Queue) Drop(ctx context.Context, id int64) error

Drop cancels a queued row.

func (*Queue) Enqueue

func (q *Queue) Enqueue(ctx context.Context, items ...Item) error

Enqueue adds items and wakes the queue.

func (*Queue) EnqueueTx

func (q *Queue) EnqueueTx(ctx context.Context, tx Execer, items ...Item) (int, error)

EnqueueTx adds items inside a transaction; call Wake after commit. Issue rows already queued for the same contact are skipped.

func (*Queue) List

func (q *Queue) List(ctx context.Context, status string, limit int) ([]Entry, error)

List shows rows of a status ("queued" by default).

func (*Queue) Retry

func (q *Queue) Retry(ctx context.Context, id int64) (int, error)

Retry makes queued rows (one, or all when id is 0) due now; failed rows are requeued too.

func (*Queue) Run

func (q *Queue) Run(ctx context.Context) error

Run delivers until ctx ends.

func (*Queue) Stats

func (q *Queue) Stats(ctx context.Context) (Status, error)

Stats reports queue counts.

func (*Queue) Wake

func (q *Queue) Wake()

Wake makes the queue look for due rows now.

type Relay

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

Relay submits everything to one smarthost with authentication, keeping a few connections open between messages.

func ParseRelay

func ParseRelay(rawURL, password, localName string) (*Relay, error)

ParseRelay builds a Relay from smtps://user@host:port or smtp://user@host:port.

func (*Relay) Close

func (r *Relay) Close()

Close drops idle connections.

func (*Relay) Name

func (r *Relay) Name() string

Name describes the relay for status output.

func (*Relay) Ping

func (r *Relay) Ping(ctx context.Context) error

Ping connects, logs in and says goodbye, to test the configuration.

func (*Relay) Relayed

func (r *Relay) Relayed() bool

Relayed reports that mail goes through a smarthost, which may rewrite some header fields (see mailauth.RelayHeaders).

func (*Relay) Send

func (r *Relay) Send(ctx context.Context, from, rcpt string, raw []byte) error

Send submits one message.

type Row

type Row struct {
	ID        int64
	Kind      string
	DomainID  int64
	IssueID   int64
	ContactID int64
	MemberID  int64
	Blob      string
	EnvFrom   string
	Rcpt      string
	Attempts  int
	Created   time.Time
}

Row is a queued message being sent.

type STSCache

type STSCache struct {
	DNS    dnsx.Resolver
	Client *http.Client
	// contains filtered or unexported fields
}

STSCache fetches and remembers MTA-STS policies.

func (*STSCache) Get

func (c *STSCache) Get(ctx context.Context, domain string) *STSPolicy

Get returns domain's policy, or nil when it has none or it cannot be fetched.

type STSPolicy

type STSPolicy struct {
	Mode   string // enforce, testing, none
	MX     []string
	MaxAge time.Duration
}

STSPolicy is a recipient domain's MTA-STS policy (RFC 8461).

func ParseSTSPolicy

func ParseSTSPolicy(r io.Reader) *STSPolicy

ParseSTSPolicy reads a policy file.

func (*STSPolicy) Enforce

func (p *STSPolicy) Enforce() bool

Enforce reports whether the policy must be followed.

func (*STSPolicy) Matches

func (p *STSPolicy) Matches(host string) bool

Matches reports whether host is an allowed MX; "*.example.com" matches one label.

type Status

type Status struct {
	Queued   int    `json:"queued"`
	Retrying int    `json:"retrying"`
	Sent24h  int    `json:"sent_24h"`
	Failed24 int    `json:"failed_24h"`
	Bounce24 int    `json:"bounced_24h"`
	Oldest   string `json:"oldest_queued,omitempty"`
	// Down says why the relay or direct delivery refuses everything, and
	// since when; mail waits without using up its attempts.
	Down      string `json:"transport_down,omitempty"`
	DownSince string `json:"transport_down_since,omitempty"`
}

Status summarizes the queue.

type Transport

type Transport interface {
	Send(ctx context.Context, from, rcpt string, raw []byte) error
	Close()
	Name() string
}

Transport hands one message to the next hop.

Jump to

Keyboard shortcuts

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