mail

package
v0.21.1 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package mail sends what an application has to say to somebody.

The shape is Laravel's, because a developer coming from there should recognise it: a Mailable declares an Envelope and a Content, a Mailer sends it, and the transport behind the Mailer is configuration rather than a decision the calling code makes.

type WelcomeEmail struct{ Name, Link string }

func (m WelcomeEmail) Envelope() mail.Envelope {
	return mail.Envelope{Subject: "Welcome"}
}

func (m WelcomeEmail) Content() mail.Content {
	return mail.Content{View: "mail.welcome", Data: m}
}

mailer.To("you@example.com").Send(ctx, WelcomeEmail{Name: "Ada"})

What is deliberately absent

No facade, no `Mail::` global, and no queue by default. Laravel queues a mailable when it implements ShouldQueue, which is an interface the class opts into at a distance; here sending on the queue is `jobs.Dispatch` with a job that sends, and the difference is visible at the call site.

No markdown mailables. Laravel has them because Blade cannot easily produce both an HTML and a text part; kyse produces whatever the view produces, and a second templating language for e-mail is the second way to draw a page that RULE 9 refuses.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoRecipient = errors.New("mail: no recipient")

ErrNoRecipient is returned by Send when nobody was addressed. It is an error rather than a silent no-op: a message with no recipient is a message somebody meant to send.

Functions

func Render

func Render(m Message) string

Render turns a message into the bytes an SMTP server receives.

It is exported because a transport that speaks a provider's HTTP API does not need it and a transport that speaks SMTP does, and both live outside this package once the adapters exist.

What it gets right that a Sprintf does not

A header carrying a non-ASCII subject has to be encoded, or the client shows mojibake -- and "Você tem uma fatura" is the first subject anybody writes here. mime.QEncoding does it, and does nothing when the text is plain ASCII.

A body line longer than 998 bytes is refused by the protocol, and an HTML document is one long line often enough. quoted-printable folds it.

A line consisting of a single dot ends the message: a body containing one is a body that is silently truncated, and the transfer encoding removes that too.

Types

type Address

type Address struct {
	// Email is the address itself, and the only required half.
	Email string
	// Name is what a client shows instead of the address. Empty is fine.
	Name string
}

Address is one mailbox, with the display name that goes in front of it.

func (Address) String

func (a Address) String() string

String renders the address the way a header carries it.

func (Address) Valid

func (a Address) Valid() bool

Valid reports whether the address parses. It is checked before a transport is asked to do anything, so a typo fails at the call rather than as a bounce three minutes later.

type Array

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

Array keeps what was sent, for a test to read.

It is safe for concurrent use, because a test that sends from two goroutines and reads from a third is a test that would otherwise fail under -race for a reason that has nothing to do with what it is proving.

func (*Array) Last

func (a *Array) Last() (Message, bool)

Last is the most recent message, and whether there was one.

func (*Array) Name

func (*Array) Name() string

Name identifies the transport in a log line.

func (*Array) Reset

func (a *Array) Reset()

Reset forgets everything. A test that shares a transport between cases calls it, and one that does not share it does not need to.

func (*Array) Send

func (a *Array) Send(_ context.Context, m Message) error

Send records the message.

func (*Array) Sent

func (a *Array) Sent() []Message

Sent is everything sent so far, oldest first.

type Content

type Content struct {
	// View is the HTML part, by the name the view is registered under.
	View string

	// TextView is the plain-text part, also by view name.
	//
	// It exists because Text alone did not do the job it was written for. A
	// project generated a mail/password-reset-text view, and nothing could ever
	// send it: the only way in was a Go string literal, so the view sat in the
	// tree looking wired while every message went out HTML-only. Found by audit.
	//
	// A message with no text part is filed as spam more often, and every client
	// that cannot render HTML shows nothing at all.
	TextView string

	// Text is the plain-text part as a literal, for a message short enough that
	// a view would be ceremony. TextView wins when both are set.
	Text string

	// Data is what both parts render from.
	Data any
}

Content is what the body is made of.

A view name and its data, rather than a string: the message is drawn by the same view layer as a page, so a field that does not exist is a compile error and interpolation is escaped by construction.

type Envelope

type Envelope struct {
	From    Address
	To      []Address
	CC      []Address
	BCC     []Address
	ReplyTo []Address

	Subject string

	// Tags and Metadata are carried by the transports that support them and
	// dropped by the ones that do not. They are how a provider's dashboard
	// groups "password resets" apart from "invoices".
	Tags     []string
	Metadata map[string]string
}

Envelope is who a message is from, who it is to, and what it says it is.

type ErrRetryable added in v0.19.0

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

ErrRetryable marks a failure worth trying again.

A 429 or a 5xx from a provider is not the same event as a rejected address, and treating them alike is how a verification e-mail is silently lost during a rate limit. A job that sends checks for this and reschedules; a request that sends inline reports it and moves on.

func (ErrRetryable) Error added in v0.19.0

func (e ErrRetryable) Error() string

func (ErrRetryable) Unwrap added in v0.19.0

func (e ErrRetryable) Unwrap() error

type Log

type Log struct{}

Log writes the message to the log instead of sending it.

It is the development default, and what makes `aru dev` work with nothing installed. The whole body is logged, because the reason to read it is to follow the link inside.

func (Log) Name

func (Log) Name() string

Name identifies the transport in a log line.

func (Log) Send

func (Log) Send(ctx context.Context, m Message) error

Send logs the message.

type Mailable

type Mailable interface {
	Envelope() Envelope
	Content() Content
}

Mailable is anything that knows how to describe itself as a message.

type Mailer

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

Mailer sends a Mailable through a Transport.

func New

func New(t Transport, r Renderer, from Address) *Mailer

New returns a Mailer.

func (*Mailer) To

func (m *Mailer) To(addresses ...string) *Pending

To starts a message to one or more addresses.

mailer.To("you@example.com").Send(ctx, WelcomeEmail{})

It returns a pending message rather than sending, so cc and bcc chain in the order they are read.

func (*Mailer) ToAddress

func (m *Mailer) ToAddress(addresses ...Address) *Pending

ToAddress is To for a recipient whose display name is known.

func (*Mailer) Transport

func (m *Mailer) Transport() Transport

Transport is which one is wired, for a health check or a log line.

type Message

type Message struct {
	Envelope
	HTML string
	Text string
}

Message is what a Transport receives: an envelope and the two rendered parts.

The transport never sees a view name or a Mailable. Rendering happens once, in the Mailer, so a transport cannot render differently from another one.

type Pending

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

Pending is a message being addressed.

func (*Pending) BCC

func (p *Pending) BCC(addresses ...string) *Pending

BCC adds recipients nobody else sees.

func (*Pending) CC

func (p *Pending) CC(addresses ...string) *Pending

CC and BCC add recipients.

func (*Pending) Send

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

Send renders the mailable and hands it to the transport.

It is synchronous. Sending on the queue is a job that calls this, and that is deliberate: a call that sometimes blocks for two seconds and sometimes does not, decided by an interface the mailable implements somewhere else, is a call nobody can reason about from the line they are reading.

type Renderer

type Renderer interface {
	RenderToString(name string, data any) (string, error)
}

Renderer draws the view a Content names.

An interface here rather than the view package directly, because mail is imported by the modules that send and importing the view package from all of them would put the whole view registry behind every one. framework/view satisfies it.

type Resend added in v0.19.0

type Resend struct {
	// Key is the API key, `re_...`. It comes from the environment and never from
	// a literal -- a key in source is a key in every clone of the repository.
	Key string

	// Endpoint overrides the API, for a test. Empty is resend.com.
	Endpoint string

	// Timeout bounds the request. Without one a hung provider holds the request
	// that triggered it for as long as the provider likes.
	Timeout time.Duration

	// Client is the HTTP client. Empty builds one with Timeout.
	Client *http.Client
}

Resend sends through resend.com.

It is the default recommendation for an application that has outgrown the log transport: a domain, a DNS record and an API key, and no server to run.

func (Resend) Name added in v0.19.0

func (Resend) Name() string

Name identifies the transport in a log line.

func (Resend) Send added in v0.19.0

func (t Resend) Send(ctx context.Context, m Message) error

Send posts the message.

type SMTP

type SMTP struct {
	// Host and Port are the server. 587 is submission with STARTTLS, which is
	// what a provider gives you; 25 is server-to-server and is usually blocked.
	Host string
	Port string

	// Username and Password authenticate. Both empty sends unauthenticated,
	// which is right for a local relay and wrong for anything reachable.
	Username string
	Password string

	// Timeout bounds the whole exchange. Without one a hung server holds the
	// request that triggered it until the client gives up -- and net/smtp has no
	// deadline of its own.
	Timeout time.Duration
}

SMTP sends over SMTP, with STARTTLS.

func (SMTP) Name

func (SMTP) Name() string

Name identifies the transport in a log line.

func (SMTP) Send

func (t SMTP) Send(ctx context.Context, m Message) error

Send delivers the message.

type SendGrid added in v0.19.0

type SendGrid struct {
	// Key is the API key, `SG....`.
	Key string

	// Endpoint overrides the API, for a test. Empty is sendgrid.com.
	Endpoint string

	// Timeout bounds the request.
	Timeout time.Duration

	// Client is the HTTP client. Empty builds one with Timeout.
	Client *http.Client
}

SendGrid sends through sendgrid.com.

The second provider rather than the only one, because a transport with one implementation is an interface nobody has proved is an interface.

func (SendGrid) Name added in v0.19.0

func (SendGrid) Name() string

Name identifies the transport in a log line.

func (SendGrid) Send added in v0.19.0

func (t SendGrid) Send(ctx context.Context, m Message) error

Send posts the message.

type Transport

type Transport interface {
	Send(ctx context.Context, m Message) error
	// Name is what appears in a log line and on the debug console.
	Name() string
}

Transport delivers a rendered message.

One method, so writing one is small: an adapter for a provider is a POST and an error, and everything above it -- addressing, rendering, validation -- has already happened.

Jump to

Keyboard shortcuts

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