mailer

package
v0.4.0 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

View Source
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

func Preview(newMailable func(r *http.Request) Mailable) http.Handler

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

func Queue(ctx context.Context, mailable Mailable, opts ...queue.DispatchOption) error

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

func Send(ctx context.Context, mailable Mailable) error

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})

func URL

func URL(ctx context.Context, path string) (string, error)

URL returns the absolute URL of path (an absolute path, "/orders/1") on the app's public URL (APP_URL), for links in emails, which leave the app:

<a href={ mailer.URL(ctx, "/orders/"+id) }>Your order</a>

func WithMailer

func WithMailer(ctx context.Context, m *Mailer) context.Context

WithMailer returns ctx with m, for Send and Queue. ForApp makes the mailer available in every context the app creates.

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"}.

func (Address) String

func (a Address) String() string

String formats a as in a header: "Ada Lovelace" <ada@example.com>, the name quoted, or encoded if it isn't ASCII.

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.

func LoadConfig

func LoadConfig(src config.Source) (Config, error)

LoadConfig reads the MAIL_* settings.

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

func ForApp(app *anetos.App, drivers ...Driver) (*Mailer, error)

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 From

func From(ctx context.Context) (*Mailer, error)

From returns the mailer in ctx.

func New

func New(t Transport, opts ...Option) *Mailer

New returns a mailer that sends with t.

func (*Mailer) Observe

func (m *Mailer) Observe(fn func(ctx context.Context, r Record))

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) Queue

func (m *Mailer) Queue(ctx context.Context, mailable Mailable, opts ...queue.DispatchOption) error

Queue is the function Queue with this mailer.

func (*Mailer) Render

func (m *Mailer) Render(ctx context.Context, mailable Mailable) (*Outgoing, error)

Render builds and renders mailable without sending it: for previews and tests.

func (*Mailer) Send

func (m *Mailer) Send(ctx context.Context, mailable Mailable) error

Send builds, renders and sends mailable now.

func (*Mailer) Transport

func (m *Mailer) Transport() 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) Reset

func (t *MemoryTransport) Reset()

Reset forgets the emails sent.

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.

func (*Message) Build

func (m *Message) Build(context.Context) (*Message, error)

Build implements Mailable: a message is its own.

type Option

type Option func(*Mailer)

Option configures a Mailer made with New.

func BaseURL

func BaseURL(url string) Option

BaseURL sets the app's public URL, for URL.

func DefaultFrom

func DefaultFrom(a Address) Option

DefaultFrom sets the sender of messages without one.

func WithLogger

func WithLogger(l *slog.Logger) Option

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

func (o *Outgoing) MIME() []byte

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

func (o *Outgoing) Recipients() []string

Recipients returns the addresses of To, Cc and Bcc: the envelope's recipients.

func (*Outgoing) Validate

func (o *Outgoing) Validate() error

Validate checks that the email can be sent: a sender and a recipient, valid addresses, no line breaks in the subject, names and headers, and no header line over the length limit. Transports check it again.

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.

Jump to

Keyboard shortcuts

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