Documentation
¶
Overview ¶
Package mail sends what an application has to say to somebody.
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.
This package is a bridge. It is removed in v1.0.0; import github.com/arandu-io/hesape/mail directly.
The components moved to github.com/arandu-io/hesape, under new names, and this package is now the old names pointing at them. It answers to two hesape packages:
hesape/mail the vocabulary and the Mailer: Mailable, Envelope,
Content, Message, Render, ErrNoRecipient, ErrRetryable
hesape/mail/transport the five transports that shipped here: SMTP, Log,
Array, Resend, SendGrid
The death date above is what keeps this from being a second way to import one type. Nothing here holds an implementation: no message is rendered, no address is parsed and no byte is written by a line in this package. Where a name survived the move it is a Go alias, and where the design diverged it is an envelope that translates and nothing more.
The envelopes, and what diverged ¶
The split runs deeper here than in most of the collection, because the type at the bottom changed shape: hesape spells a mailbox Address{Address, Name}, and this package has always spelled it Address{Email, Name}. Every type that carries an address therefore had to stay declared here rather than become an alias -- Address, Envelope, Message -- and with them the two interfaces that mention those types, Mailable and Transport.
Address Email is Address there
Envelope carries this package's Address, and has no Using field
Content TextView is Text there, which is a view name on both sides, and
Data is With; the literal Text has no counterpart and is applied
to the built message instead
Message carries this package's Envelope
Transport Send answers an error where hesape answers (SentMessage, error)
Mailable answers this package's Envelope and Content
Mailer hesape's takes a name and an event dispatcher, and its To takes
one polymorphic argument where this one takes strings
Pending PendingMail there, with a whole fluent surface
Array Sent and Reset are Messages and Flush there
Three differences a caller can observe ¶
A Content whose Data is nil renders against the mailable rather than against nil: hesape's BuildViewData falls back to the mailable when no view data was named, and the value it falls back to is this package's adapter. A Content that names a View has always named its Data too, so this is the degenerate case rather than a path anything takes.
Recipients are deduplicated, keeping the last spelling of a repeated address, which is where a display name comes from. This package used to send the same address twice.
The recipients named on the call arrive before the ones named on the envelope, where they used to arrive after. Nothing reads that order except a transport writing a header.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoRecipient = hmail.ErrNoRecipient
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 ¶
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.
A wrapper and not an alias: a plain function has no alias form, and the argument is this package's Message rather than hesape's.
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.
Declared here rather than aliased: the first field is called Address in hesape, and every call site in the collection writes mail.Address{Email: ...}.
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. The lock and the slice are hesape's: this type holds no storage of its own.
Its three readers were renamed on the way over -- Sent is Messages there and Reset is Flush -- so the three below are the old names reaching the new ones.
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.
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 is Text in hesape,
// where it means the same thing.
//
// 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.
//
// hesape has no field for a literal text body, so the bridge applies this
// one to the built message rather than to the content it hands over.
Text string
// Data is what both parts render from. It is With in hesape.
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 = hmail.ErrRetryable
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.
The alias is what keeps it one type: an errors.As against this name matches the value a hesape transport wrapped.
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.
It writes wherever the context is logging, as it always did. What changed is which package the context is asked: it is hesape/log now rather than framework/observability, and framework/observability is a bridge over that same package, so a request logger installed by either is the one found here.
type Mailer ¶
type Mailer struct {
// contains filtered or unexported fields
}
Mailer sends a Mailable through a Transport.
func (*Mailer) To ¶
To starts a message to one or more addresses.
It returns a pending message rather than sending, so cc and bcc chain in the order they are read.
type Message ¶
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.
hesape's Message carries attachments, headers, embedded parts and a priority as well. None of them has ever been reachable through this package, so a message crossing this boundary loses nothing it was carrying.
type Pending ¶
type Pending struct {
// contains filtered or unexported fields
}
Pending is a message being addressed.
func (*Pending) Send ¶
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 ¶
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.
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.
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.
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.
Declared here with the old shape, and not aliased: hesape's Send answers a receipt as well as an error, and an alias would compile in this module while silently refusing every Transport written in another one.