Documentation
¶
Overview ¶
Package mailer sends email. An email is a Mailable: a type that builds a Message (recipients, subject, an HTML body from a templ component, a text body, attachments) from its fields. Send sends it now; Queue renders it now and sends it from a queue job, with retries.
m, err := mailer.ForApp(app) // MAIL_DRIVER: log (default), smtp, memory, or a driver module's
err = mailer.Send(ctx, mails.Welcome{User: u})
err = mailer.Queue(ctx, mails.Receipt{Order: o}, queue.OnQueue("emails"))
Transports send rendered emails: the log (development), SMTP, memory (tests) and, in other modules, email APIs (plugins/postmark). The mailer travels in the context, like the database and the cache.
The package is named mailer, not mail, so it doesn't shadow net/mail.
Index ¶
- Variables
- func Preview(newMailable func(r *http.Request) Mailable) http.Handler
- func Queue(ctx context.Context, mailable Mailable, opts ...queue.DispatchOption) error
- func Send(ctx context.Context, mailable Mailable) error
- func URL(ctx context.Context, path string) (string, error)
- func WithMailer(ctx context.Context, m *Mailer) context.Context
- type Address
- type Attachment
- type Config
- type Driver
- type LogTransport
- type Mailable
- type Mailer
- func (m *Mailer) Observe(fn func(ctx context.Context, r Record))
- func (m *Mailer) Queue(ctx context.Context, mailable Mailable, opts ...queue.DispatchOption) error
- func (m *Mailer) Render(ctx context.Context, mailable Mailable) (*Outgoing, error)
- func (m *Mailer) Send(ctx context.Context, mailable Mailable) error
- func (m *Mailer) Transport() Transport
- type MemoryTransport
- type Message
- type Option
- type Outgoing
- type Record
- type SMTPTransport
- type Transport
Constants ¶
This section is empty.
Variables ¶
var ErrNoMailer = errors.New("mailer: no mailer in the context: call mailer.ForApp at startup, or mailer.WithMailer")
ErrNoMailer is returned by From (and Send, Queue) when the context has no mailer.
Functions ¶
func Preview ¶
Preview returns a handler that shows the HTML body of the mailable newMailable returns, rendered with the request's context, for viewing emails in a browser during development. Don't serve it in production: it shows whatever the mailable puts in the email.
if app.Config().Env.IsDevelopment() {
r.HandleStd("GET", "/dev/mail/receipt", mailer.Preview(func(r *http.Request) mailer.Mailable { return mails.Receipt{…} }))
}
func Queue ¶
Queue builds and renders mailable now, with ctx (a request's, say), and dispatches a queue job that sends it, with the queue's retries: opts are the dispatch's (queue.OnQueue, queue.Delay, queue.AfterCommit). It needs the app's queue (queue.ForApp). The job carries the rendered email, attachments included (and a failed job keeps it): send big files from a job of your own, with Send. The email's Date is when the job sends it.
err := mailer.Queue(c, mails.Receipt{Order: o}, queue.OnQueue("emails"))
func Send ¶
Send builds, renders and sends mailable now, with the mailer in ctx, and returns the transport's error:
err := mailer.Send(ctx, mails.Welcome{User: u})
Types ¶
type Address ¶
type Address struct {
// Name is the display name; it may be empty.
Name string `json:"name,omitempty"`
// Address is the email address, "ada@example.com".
Address string `json:"address"`
}
Address is an email address with an optional display name: mailer.Address{Name: "Ada Lovelace", Address: "ada@example.com"}.
type Attachment ¶
type Attachment struct {
// Filename is the file's name, as the recipient sees it.
Filename string `json:"filename"`
// ContentType is the file's media type. Default: from the
// filename's extension, or application/octet-stream.
ContentType string `json:"content_type,omitempty"`
// Data is the file's content.
Data []byte `json:"data"`
// ContentID makes the file inline, shown in the HTML body by
// <img src="cid:ContentID">, rather than attached.
ContentID string `json:"content_id,omitempty"`
}
Attachment is a file sent with an email.
type Config ¶
type Config struct {
// Driver is how emails are sent: log (written to the app's log),
// smtp, memory (kept, for tests), or one passed to ForApp
// (postmark). MAIL_DRIVER, default log.
Driver string `env:"MAIL_DRIVER" default:"log"`
// FromAddress is the sender of messages without one.
// MAIL_FROM_ADDRESS.
FromAddress string `env:"MAIL_FROM_ADDRESS"`
// FromName is the sender's name. MAIL_FROM_NAME, default APP_NAME.
FromName string `env:"MAIL_FROM_NAME"`
// SMTPURL is the SMTP server: smtp://user:password@host:587 (with
// STARTTLS) or smtps://…:465 (TLS); a Secret, since it holds the
// password. MAIL_SMTP_URL, default smtp://127.0.0.1:1025 (Mailpit,
// for development).
SMTPURL anetos.Secret `env:"MAIL_SMTP_URL" default:"smtp://127.0.0.1:1025"`
}
Config selects and configures the app's mail transport.
type Driver ¶
type Driver struct {
// Name is the value of MAIL_DRIVER that selects the driver.
Name string
// Open returns the transport for the app. If it implements
// io.Closer, it is closed when the app shuts down.
Open func(app *anetos.App, cfg Config) (Transport, error)
}
Driver opens a transport for ForApp. The log, smtp and memory drivers are built in; driver modules provide others (postmark.Driver()).
func LogDriver ¶
func LogDriver() Driver
LogDriver writes emails to the app's log instead of sending them (MAIL_DRIVER=log, the default): for development. In production it leaves the bodies out, which may hold sign-in or reset links.
func MemoryDriver ¶
func MemoryDriver() Driver
MemoryDriver keeps emails in memory (MAIL_DRIVER=memory): for tests, which check them with the mailer's *MemoryTransport.
func SMTPDriver ¶
func SMTPDriver() Driver
SMTPDriver sends emails to the SMTP server of MAIL_SMTP_URL (MAIL_DRIVER=smtp).
type LogTransport ¶
type LogTransport struct {
// contains filtered or unexported fields
}
LogTransport writes emails to a log instead of sending them: the sender, recipients, subject and text body, at Info.
func NewLogTransport ¶
func NewLogTransport(log *slog.Logger) *LogTransport
NewLogTransport returns a transport that writes emails to log.
func (*LogTransport) Send ¶
func (t *LogTransport) Send(ctx context.Context, m *Outgoing) error
Send implements Transport.
func (*LogTransport) WithoutBodies ¶ added in v0.3.0
func (t *LogTransport) WithoutBodies() *LogTransport
WithoutBodies returns the transport logging everything but the emails' bodies, which may hold sign-in or reset links: what MAIL_DRIVER=log does in production, where logs are read by more people.
type Mailable ¶
type Mailable interface {
// Build returns the message to send; ctx is Send's (or Queue's).
Build(ctx context.Context) (*Message, error)
}
Mailable is an email: a type that builds its Message from its fields and the context (which has the app's values: the database, …).
type Welcome struct{ User models.User }
func (w Welcome) Build(ctx context.Context) (*mailer.Message, error) {
return &mailer.Message{
To: []mailer.Address{{Name: w.User.Name, Address: w.User.Email}},
Subject: "Welcome to Blog",
HTML: views.WelcomeEmail(w.User),
}, nil
}
A *Message is a Mailable too.
type Mailer ¶
type Mailer struct {
// contains filtered or unexported fields
}
Mailer sends emails with a transport. Create it with ForApp (or New); send with Send or Queue, which find it in the context.
func ForApp ¶
ForApp sets up the app's mailer from the MAIL_* settings: it opens the transport MAIL_DRIVER names (log, smtp and memory are built in; pass others, such as postmark.Driver()) and makes the mailer available in every context the app creates, for Send and Queue. If the app has a queue (queue.ForApp, before or after), it registers the job that sends queued emails.
m, err := mailer.ForApp(app, postmark.Driver())
func (*Mailer) Observe ¶
Observe calls fn with each email the mailer sends with Send (once the transport took it) or queues with Queue (once the job is dispatched: with queue.AfterCommit, after the commit; not if the transaction rolls back; with the sync driver, once the job sent it) from now on, for tests and instrumentation. fn must be quick and safe for concurrent use. anetostest uses it to record mail.
func (*Mailer) Render ¶
Render builds and renders mailable without sending it: for previews and tests.
func (*Mailer) Transport ¶
Transport returns the mailer's transport: in tests, a *MemoryTransport to check what was sent.
type MemoryTransport ¶
type MemoryTransport struct {
// contains filtered or unexported fields
}
MemoryTransport keeps the emails it is given, for tests:
sent := anetos.MustResolve[*mailer.Mailer](app).Transport().(*mailer.MemoryTransport).Sent()
func NewMemoryTransport ¶
func NewMemoryTransport() *MemoryTransport
NewMemoryTransport returns an empty memory transport.
func (*MemoryTransport) Send ¶
func (t *MemoryTransport) Send(ctx context.Context, m *Outgoing) error
Send implements Transport.
func (*MemoryTransport) Sent ¶
func (t *MemoryTransport) Sent() []*Outgoing
Sent returns the emails sent, oldest first.
type Message ¶
type Message struct {
// From is the sender. Default: MAIL_FROM_ADDRESS and MAIL_FROM_NAME.
From Address
// To, Cc and Bcc are the recipients: at least one in all. Bcc
// recipients get the email without being listed in it.
To, Cc, Bcc []Address
// ReplyTo is where replies go, if not to From.
ReplyTo []Address
// Subject is the subject line.
Subject string
// HTML is the HTML body: a templ component (or any view.Component),
// rendered with the context of Send. Use inline styles: many mail
// clients ignore style sheets.
HTML view.Component
// Text is the plain-text body, as is (build it with fmt or
// text/template: templ would HTML-escape it). Without it, the HTML
// body's text is used: paragraphs, lists, and links followed by their
// URL. At least one of HTML and Text is required.
Text string
// Attachments are files sent with the email.
Attachments []Attachment
// Headers are extra headers, such as List-Unsubscribe. They can't
// replace the ones the message sets (From, To, Subject, Date, …).
Headers map[string]string
// Tag labels the email for API drivers that group emails by it
// (Postmark); SMTP ignores it.
Tag string
// Metadata labels the email with pairs that API drivers keep with it
// (Postmark); SMTP sends them as X-Metadata-<key> headers. Keys are
// letters, digits, - and _.
Metadata map[string]string
}
Message is an email to send.
type Option ¶
type Option func(*Mailer)
Option configures a Mailer made with New.
func DefaultFrom ¶
DefaultFrom sets the sender of messages without one.
func WithLogger ¶
WithLogger sets the mailer's logger. Default slog.Default().
type Outgoing ¶
type Outgoing struct {
// MessageID is the Message-ID header's value, without the angle
// brackets: unique, and kept if the email is sent again.
MessageID string `json:"message_id"`
// Date is when the email is sent: when it was rendered, or, for
// queued mail, when the job sends it.
Date time.Time `json:"date"`
// From is the sender: the message's, or the mailer's default.
From Address `json:"from"`
// To is the main recipients.
To []Address `json:"to,omitempty"`
// Cc is the copied recipients.
Cc []Address `json:"cc,omitempty"`
// Bcc is the hidden recipients: in the SMTP envelope, not the
// message.
Bcc []Address `json:"bcc,omitempty"`
// ReplyTo is where replies go.
ReplyTo []Address `json:"reply_to,omitempty"`
// Subject is the subject line.
Subject string `json:"subject"`
// HTML is the HTML body; it may be empty.
HTML string `json:"html,omitempty"`
// Text is the plain-text body.
Text string `json:"text"`
// Attachments are as in [Message], with their content types set.
Attachments []Attachment `json:"attachments,omitempty"`
// Headers are the message's extra headers.
Headers map[string]string `json:"headers,omitempty"`
// Tag is the message's tag.
Tag string `json:"tag,omitempty"`
// Metadata is the message's metadata.
Metadata map[string]string `json:"metadata,omitempty"`
}
Outgoing is a rendered email, what transports send: a Message with its sender, bodies and ID filled in. It is JSON, for queued mail.
func (*Outgoing) MIME ¶
MIME returns the email as an RFC 5322 message, for SMTP and for API drivers that take raw messages: the headers (without Bcc), then the bodies, inline files and attachments as MIME parts. Line endings are CRLF. Call Outgoing.Validate first.
func (*Outgoing) Recipients ¶
Recipients returns the addresses of To, Cc and Bcc: the envelope's recipients.
type Record ¶
type Record struct {
// Mailable is the mailable given to Send or Queue.
Mailable Mailable
// Message is the email it rendered.
Message *Outgoing
// Queued says it went through Queue: a job sends it.
Queued bool
}
Record is an email the mailer sent or queued, for Mailer.Observe.
type SMTPTransport ¶
type SMTPTransport struct {
// contains filtered or unexported fields
}
SMTPTransport sends emails to an SMTP server, one connection per email.
func NewSMTPTransport ¶
func NewSMTPTransport(rawURL string) (*SMTPTransport, error)
NewSMTPTransport returns a transport for the server of rawURL:
- smtp://user:password@smtp.example.com:587 upgrades the connection with STARTTLS, which the server must offer, unless the host is local (127.0.0.1, ::1, localhost: Mailpit, a relay on the machine), where it is used if offered. Default port 587.
- smtps://user:password@smtp.example.com:465 uses TLS from the start. Default port 465.
The user and password, URL-encoded, are sent with AUTH PLAIN (or LOGIN if the server offers only that), only over TLS or to a local host. Query parameters: tls=none never upgrades (a relay on a private network that has no TLS, which can't take a password); timeout=30s bounds each email (default 30s); local_name=host is the name sent in EHLO (default: the machine's).
func (*SMTPTransport) Send ¶
func (t *SMTPTransport) Send(ctx context.Context, m *Outgoing) error
Send implements Transport: it connects, sends m and quits. Replies in the 500s (a rejected recipient, refused credentials; but not 552 to a recipient, which RFC 5321 says to treat as temporary) and errors in the message or the settings are permanent (queue.Permanent). One rejected recipient fails the whole email.
type Transport ¶
type Transport interface {
// Send sends m, which [Outgoing.Validate] accepted. An error that
// retrying won't fix (a rejected address, a refused message) should
// be queue.Permanent, so a queued email fails at once.
Send(ctx context.Context, m *Outgoing) error
}
Transport sends rendered emails: SMTP, an API, the log. Drivers provide them; MemoryTransport keeps them, for tests.