mail

package
v0.31.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 7 Imported by: 0

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

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

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.

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

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

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

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

func (*Array) Name

func (a *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(ctx 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 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.

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.

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.

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) 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 adds 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 = hmail.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.

func (Resend) Name added in v0.19.0

func (t 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 (t 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 (t 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.

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.

Jump to

Keyboard shortcuts

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