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
- Variables
- func Backoff(attempts int) time.Duration
- func IsHardBounce(code int, enhanced, message string) bool
- func Probe(ctx context.Context, host string) error
- type Builder
- type Direct
- type Entry
- type Execer
- type Failure
- type Hooks
- type Item
- type Queue
- func (q *Queue) Drop(ctx context.Context, id int64) error
- func (q *Queue) Enqueue(ctx context.Context, items ...Item) error
- func (q *Queue) EnqueueTx(ctx context.Context, tx Execer, items ...Item) (int, error)
- func (q *Queue) List(ctx context.Context, status string, limit int) ([]Entry, error)
- func (q *Queue) Retry(ctx context.Context, id int64) (int, error)
- func (q *Queue) Run(ctx context.Context) error
- func (q *Queue) Stats(ctx context.Context) (Status, error)
- func (q *Queue) Wake()
- type Relay
- type Row
- type STSCache
- type STSPolicy
- type Status
- type Transport
Constants ¶
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 ¶
var ErrSkip = errors.New("skip")
ErrSkip cancels a row instead of sending it.
Functions ¶
func IsHardBounce ¶
IsHardBounce decides whether a permanent failure means the address is gone (stop sending to it) rather than a policy or content rejection.
Types ¶
type Builder ¶
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.
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.
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) EnqueueTx ¶
EnqueueTx adds items inside a transaction; call Wake after commit. Issue rows already queued for the same contact are skipped.
func (*Queue) Retry ¶
Retry makes queued rows (one, or all when id is 0) due now; failed rows are requeued too.
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 ¶
ParseRelay builds a Relay from smtps://user@host:port or smtp://user@host:port.
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.
type STSPolicy ¶
STSPolicy is a recipient domain's MTA-STS policy (RFC 8461).
func ParseSTSPolicy ¶
ParseSTSPolicy reads a policy file.
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.