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 ¶
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.
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 ¶
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).
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.
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 ¶
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.