email

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 47 Imported by: 0

Documentation

Overview

Package email builds bounded messages, renders typed templates and submits through borrowed transports. Acceptance is not proof of inbox delivery. Jobs and transactional publication remain owned by package jobs.

Index

Constants

View Source
const MaxAddressBytes = 1024
View Source
const MaxAttachments = 32
View Source
const MaxDataAttachmentBytes = 10 << 20

MaxDataAttachmentBytes bounds one in-memory attachment; the mailer's MaxAttachmentBytes and MaxMessageBytes still apply.

View Source
const MaxDisplayNameBytes = 256
View Source
const MaxHeaders = 32
View Source
const MaxMailboxBytes = 254
View Source
const MaxRecipients = 50

Variables

This section is empty.

Functions

func JobHandler

func JobHandler[P any](mailer *Mailer, build Builder[P]) jobs.Handler[P]

JobHandler plugs into Definition[P].Declare. Capture/Dispatch/Enqueue, attribution, serialization, versioning, retries and outbox routing remain the existing jobs API. Each execution reuses its stable ID as provider key. Ambiguous acceptance is terminal, including on idempotent providers: provider retention is finite and does not cover arbitrary worker redelivery intervals.

func Module

func Module(name foundation.ProviderID, key foundation.Key[*Mailer], requires []foundation.ProviderID, construct func(foundation.Resolver) (*Mailer, error)) foundation.Module

Module registers an owned mailer. Requires must name providers owning its borrowed transport and disks so sends drain before those resources close.

Types

type Address

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

Address is one validated ASCII mailbox with an optional UTF-8 display name. SMTPUTF8 local parts and IDN conversion are deliberately not implicit.

func NewAddress

func NewAddress(mailbox, name string) (Address, error)

func ParseAddress

func ParseAddress(text string) (Address, error)

func (Address) Format

func (Address) Format(s fmt.State, _ rune)

func (Address) Header

func (a Address) Header() string

Header explicitly reveals the formatted mailbox. Generic formatting is redacted.

func (Address) JSONContract

func (Address) JSONContract() contract.JSON[Address]

JSONContract is the single scalar wire declaration used by generated DTOs and contract export. Display-name mailboxes are strings, not OpenAPI email-format strings; NewAddress/ParseAddress retain the stricter runtime mailbox checks.

func (Address) LogValue

func (Address) LogValue() slog.Value

func (Address) Mailbox

func (a Address) Mailbox() string

func (Address) MarshalJSON

func (a Address) MarshalJSON() ([]byte, error)

func (Address) MarshalText

func (a Address) MarshalText() ([]byte, error)

Text configuration reuses the same mailbox parser as message construction.

func (Address) Name

func (a Address) Name() string

func (*Address) UnmarshalJSON

func (a *Address) UnmarshalJSON(data []byte) error

func (*Address) UnmarshalText

func (a *Address) UnmarshalText(data []byte) error

func (Address) Validate

func (a Address) Validate() error

type Attachment

type Attachment struct {
	Disk        storage.DiskID    `json:"disk"`
	Key         storage.ObjectKey `json:"key"`
	Filename    string            `json:"filename"`
	ContentType storage.MediaType `json:"content_type"`
	ContentID   string            `json:"content_id,omitempty"`
	Version     storage.VersionID `json:"version,omitempty"`
	IfMatch     storage.ETag      `json:"if_match,omitempty"`
}

Attachment references shared storage. Pin Version or IfMatch when queued so redelivery cannot silently send replacement content. Readers and OS paths never enter serialized email data.

func (Attachment) Format

func (Attachment) Format(s fmt.State, _ rune)

func (Attachment) LogValue

func (Attachment) LogValue() slog.Value

func (Attachment) Validate

func (a Attachment) Validate() error

type Body

type Body struct {
	Subject, Text, HTML string
	Locale              i18n.LocaleID
}

Body is rendered template output. Locale is set by LocalizedTemplate and carried to the message.

func (Body) Format

func (Body) Format(s fmt.State, _ rune)

func (Body) LogValue

func (Body) LogValue() slog.Value

func (Body) Message

func (b Body) Message(from Address, to ...Address) Message

type Builder

type Builder[P any] func(context.Context, P) (Message, error)

Builder renders one deterministic message from a declared job DTO. Capture template inputs and pinned attachment references in that DTO. Never read mutable recipient state or current time when an identical retry is required.

type Config

type Config struct {
	// From supplies Mailer.Message; explicit NewMessage senders are never replaced.
	From                                           Address
	MaxActive, MaxMessageBytes, MaxAttachmentBytes int
	Timeout                                        time.Duration
	// MessageIDDomain is the right-hand side of generated Message-ID headers.
	// Empty uses each message's sender domain.
	MessageIDDomain string
}

func DefaultConfig

func DefaultConfig() Config

func (Config) Validate

func (c Config) Validate() error

type DataAttachment

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

DataAttachment is in-memory content attached directly, for generated files such as a rendered invoice. It holds an owned copy of the bytes. It is not serializable: queued and snapshotted email must reference stored attachments.

func NewDataAttachment

func NewDataAttachment(filename string, contentType storage.MediaType, data []byte) (DataAttachment, error)

func (DataAttachment) ContentID

func (a DataAttachment) ContentID() string

func (DataAttachment) ContentType

func (a DataAttachment) ContentType() storage.MediaType

func (DataAttachment) Filename

func (a DataAttachment) Filename() string

func (DataAttachment) Format

func (DataAttachment) Format(s fmt.State, _ rune)

func (DataAttachment) LogValue

func (DataAttachment) LogValue() slog.Value

func (DataAttachment) Size

func (a DataAttachment) Size() int

func (DataAttachment) WithContentID

func (a DataAttachment) WithContentID(id string) (DataAttachment, error)

WithContentID returns an inline copy referenced as cid:id from HTML.

type Diagnostics

type Diagnostics struct {
	Active                                    int
	Closing                                   bool
	Calls, Accepted, Failed, ObserverFailures uint64
}

type Driver

type Driver interface {
	Send(context.Context, Outbound) (Receipt, error)
}

Driver performs one submission. It must classify failures, honor context, avoid implicit retries, and return only after releasing its I/O. Mailer borrows it; the application closes transport resources after Mailer.Done.

type DriverFunc

type DriverFunc func(context.Context, Outbound) (Receipt, error)

func (DriverFunc) Send

func (f DriverFunc) Send(ctx context.Context, o Outbound) (Receipt, error)

type DynamicTemplate

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

DynamicTemplate is the explicit heterogeneous-map escape hatch.

func NewDynamicTemplate

func NewDynamicTemplate(source TemplateSource) (DynamicTemplate, error)

func (DynamicTemplate) Render

func (t DynamicTemplate) Render(ctx context.Context, data map[string]any) (Body, error)

type HTTPConfig

type HTTPConfig struct {
	Endpoint  string
	Timeout   time.Duration
	Transport http.RoundTripper
}

HTTPConfig configures API adapters. Empty Endpoint selects the provider's documented URL; overrides must use HTTPS or an explicit loopback HTTP URL. A supplied RoundTripper is borrowed and must not retry submissions itself.

func DefaultHTTPConfig

func DefaultHTTPConfig() HTTPConfig

func (HTTPConfig) Format

func (HTTPConfig) Format(s fmt.State, _ rune)

type IdempotencyKey

type IdempotencyKey string

IdempotencyKey identifies one logical submission. Reuse only for an identical message. Provider support and retention are documented by each adapter.

func (IdempotencyKey) Format

func (IdempotencyKey) Format(s fmt.State, _ rune)

func (IdempotencyKey) LogValue

func (IdempotencyKey) LogValue() slog.Value

func (IdempotencyKey) Validate

func (k IdempotencyKey) Validate() error

type Kind

type Kind string

Kind describes whether a failed submission is safe to retry. Only Transient means known non-acceptance with a potentially recoverable cause.

const (
	Construction Kind = emaildiag.Construction
	Permanent    Kind = emaildiag.Permanent
	Transient    Kind = emaildiag.Transient
	Ambiguous    Kind = emaildiag.Ambiguous
)

func Classification

func Classification(err error) Kind

Classification inspects extension errors inside callback isolation. Unknown errors, incomplete error graphs, panic and Goexit fail conservatively; underlying text is never exposed. Custom error methods must return.

func (Kind) Error

func (k Kind) Error() string

type Layout

type Layout struct {
	Text, HTML string
}

Layout is a shared trusted wrapper for message bodies: its HTML and Text sources render the message's own body where they call {{template "content" .}}, with the same typed data. A layout part applies only when the message defines that part. The subject is never wrapped.

type LocalizedTemplate

type LocalizedTemplate[T any] struct {
	// contains filtered or unexported fields
}

LocalizedTemplate holds one typed template per locale and selects it along the locale set's fallback chain (for example pt-BR, then pt, then the default). The rendered Body records the locale actually used.

func NewLocalizedTemplate

func NewLocalizedTemplate[T any](locales i18n.LocaleSet, sources map[i18n.LocaleID]TemplateSource) (LocalizedTemplate[T], error)

NewLocalizedTemplate requires a template for the set's default locale; every source locale must belong to the set.

func (LocalizedTemplate[T]) Render

func (t LocalizedTemplate[T]) Render(ctx context.Context, locale i18n.LocaleID, data T) (Body, error)

Render uses the first template on locale's fallback chain; an unsupported or empty locale renders the default.

type Mailer

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

func New

func New(driver Driver, disks *storage.Registry, config Config, observer Observer) (*Mailer, error)

New borrows the transport and storage registry. It performs no I/O.

func (*Mailer) Close

func (m *Mailer) Close(ctx context.Context) error

Close cancels admission and active I/O. Its context bounds only this caller's wait; Done remains open until every callback really exits. Borrowed resources are not closed. Calling Close from an active send context returns fault.Cycle.

func (*Mailer) Done

func (m *Mailer) Done() <-chan struct{}

func (*Mailer) Format

func (*Mailer) Format(s fmt.State, _ rune)

func (*Mailer) LogValue

func (*Mailer) LogValue() slog.Value

func (*Mailer) Message

func (m *Mailer) Message(subject string, to ...Address) Message

Message starts an immutable message using this mailer's configured sender. An unconfigured sender produces a message rejected by Send's normal validation.

func (*Mailer) Send

func (m *Mailer) Send(ctx context.Context, message Message, options SendOptions) (result Result, err error)

Send waits for actual completion even when an extension ignores cancellation. A non-nil error never claims known acceptance; an accepted result remains accepted if a Finished observer fails or the context expires afterward.

func (*Mailer) Snapshot

func (m *Mailer) Snapshot() Diagnostics

type MailerName

type MailerName string

MailerName selects an instance, distinct from names of other service families.

func (MailerName) Validate

func (n MailerName) Validate() error

type Mailers

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

Mailers is immutable; default and named selection alias the same instance.

func NewMailers

func NewMailers(selected MailerName, entries ...NamedMailer) (*Mailers, error)

func (*Mailers) Default

func (r *Mailers) Default() (*Mailer, error)

func (*Mailers) DefaultName

func (r *Mailers) DefaultName() MailerName

func (*Mailers) Mailer

func (r *Mailers) Mailer(name MailerName) (*Mailer, error)

func (*Mailers) Names

func (r *Mailers) Names() []MailerName

type Message

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

Message is immutable. Builders copy changed collections; no caller map or slice is retained. Validate runs again at the submission boundary.

func NewMessage

func NewMessage(from Address, subject string, to ...Address) Message

func (Message) Attach

func (m Message) Attach(attachments ...Attachment) Message

func (Message) AttachData

func (m Message) AttachData(attachments ...DataAttachment) Message

AttachData adds in-memory attachments (see DataAttachment).

func (Message) AttachStored

func (m Message) AttachStored(file StoredAttachment) (Message, error)

AttachStored snapshots a stored file or a loaded single-file model slot into this message. It performs no reads; Send resolves bytes through the mailer's storage registry, using the captured version or ETag. Missing/unloaded files fail rather than silently omitting the attachment. Retain the stored object for the lifetime of queued mail; a reference does not prevent deletion.

func (Message) AttachUpload

func (m Message) AttachUpload(ctx context.Context, file UploadSource) (Message, error)

AttachUpload reads and closes one upload now, without persisting it. The returned message owns the bytes and survives request cleanup. Detected bytes determine its media type; the client type is only a compatible text hint. MaxDataAttachmentBytes bounds the read; mailer limits also apply at Send. Queueing/snapshotting requires stored references instead of runtime uploads.

func (Message) Attachments

func (m Message) Attachments() []Attachment

func (Message) BCC

func (m Message) BCC() []Address

func (Message) Bcc

func (m Message) Bcc(addresses ...Address) Message

func (Message) CC

func (m Message) CC() []Address

func (Message) Cc

func (m Message) Cc(addresses ...Address) Message

func (Message) DataAttachments

func (m Message) DataAttachments() []DataAttachment

func (Message) EnvelopeRecipients

func (m Message) EnvelopeRecipients() []Address

EnvelopeRecipients includes BCC. MIME headers deliberately exclude BCC.

func (Message) Format

func (Message) Format(s fmt.State, _ rune)

func (Message) From

func (m Message) From() Address

func (Message) HTML

func (m Message) HTML(html string) Message

func (Message) HTMLBody

func (m Message) HTMLBody() string

func (Message) Header

func (m Message) Header(name, value string) Message

func (Message) Headers

func (m Message) Headers() map[string]string

func (Message) Locale

func (m Message) Locale(locale i18n.LocaleID) Message

Locale records the message's language. MIME transports send it as the Content-Language header; LocalizedTemplate sets it when rendering.

func (Message) LocaleID

func (m Message) LocaleID() i18n.LocaleID

func (Message) MarshalJSON

func (Message) MarshalJSON() ([]byte, error)

MarshalJSON rejects accidental queuing of runtime messages. Queue a declared DTO containing template data and Attachment references through JobHandler.

func (Message) RecipientCount

func (m Message) RecipientCount() int

func (Message) ReplyAddresses

func (m Message) ReplyAddresses() []Address

func (Message) ReplyTo

func (m Message) ReplyTo(addresses ...Address) Message

func (Message) Subject

func (m Message) Subject() string

func (Message) Text

func (m Message) Text(text string) Message

func (Message) TextBody

func (m Message) TextBody() string

func (Message) To

func (m Message) To() []Address

func (Message) Validate

func (m Message) Validate() error

type NamedMailer

type NamedMailer struct {
	Name  MailerName
	Value *Mailer
}

NamedMailer borrows one existing instance. Selection never creates another owner.

type Notice

type Notice struct {
	Stage             Stage
	Recipients, Bytes int
	Accepted          bool
	Failure           Kind
	Duration          time.Duration
}

Notice deliberately excludes addresses, subject, body, attachment names, message IDs and keys. Finished observers cannot undo provider acceptance.

type Observer

type Observer func(context.Context, Notice) error

type Outbound

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

Outbound can only be constructed by Mailer after validation and resolution. Driver implementations receive this immutable submission snapshot. A structured submission (see StructuredDriver) carries no MIME document.

func (Outbound) Attachments

func (o Outbound) Attachments() []ResolvedAttachment

func (Outbound) Format

func (Outbound) Format(s fmt.State, _ rune)

func (Outbound) IdempotencyKey

func (o Outbound) IdempotencyKey() IdempotencyKey

func (Outbound) MIME

func (o Outbound) MIME() []byte

func (Outbound) Message

func (o Outbound) Message() Message

func (Outbound) Size

func (o Outbound) Size() int

Size is the encoded MIME size, or the estimated encoded size of bodies and attachments for a structured submission.

func (Outbound) Validate

func (o Outbound) Validate() error

type Receipt

type Receipt struct{ MessageID string }

Receipt means provider acceptance, never inbox delivery. SMTP may have no ID.

func (Receipt) Format

func (Receipt) Format(s fmt.State, _ rune)

func (Receipt) LogValue

func (Receipt) LogValue() slog.Value

func (Receipt) Validate

func (r Receipt) Validate() error

type ResolvedAttachment

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

ResolvedAttachment owns a bounded snapshot. Bytes returns a copy, never a reader into a storage resource. Drivers must not retain borrowed calls.

func (ResolvedAttachment) Bytes

func (a ResolvedAttachment) Bytes() []byte

func (ResolvedAttachment) Format

func (ResolvedAttachment) Format(s fmt.State, _ rune)

func (ResolvedAttachment) Reference

func (a ResolvedAttachment) Reference() Attachment

func (ResolvedAttachment) Size

func (a ResolvedAttachment) Size() int

type Result

type Result struct {
	Receipt        Receipt
	Accepted       bool
	ObserverFailed bool
}

type SendOptions

type SendOptions struct{ IdempotencyKey IdempotencyKey }

type Snapshot

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

Snapshot is an explicit durable, rendered message. Capture it only after resolving the intended recipient. It contains private email content and must be protected like an outbox payload. It contains no readers or executable code. Unlike a source-data job, retrying a snapshot never rerenders or changes routes.

func CaptureMessage

func CaptureMessage(message Message) (Snapshot, error)

CaptureMessage freezes a validated message within value.JSONMaxBytes. Every attachment must pin Version or IfMatch; mutable storage names cannot enter a durable rendered message. Ordinary Message JSON serialization remains denied.

func (Snapshot) Format

func (Snapshot) Format(s fmt.State, _ rune)

func (Snapshot) LogValue

func (Snapshot) LogValue() slog.Value

func (Snapshot) MarshalJSON

func (s Snapshot) MarshalJSON() ([]byte, error)

func (Snapshot) Message

func (s Snapshot) Message() (Message, error)

func (*Snapshot) UnmarshalJSON

func (s *Snapshot) UnmarshalJSON(data []byte) error

type Stage

type Stage string
const (
	Prepared Stage = "prepared"
	Finished Stage = "finished"
)

type StoredAttachment

type StoredAttachment interface {
	EmailAttachment() (Attachment, error)
}

StoredAttachment supplies one pinned reference without I/O. Foundry's attachments.File, Attachment and loaded One slots implement this contract. The caller must authorize access to the file before attaching it.

type StructuredDriver

type StructuredDriver interface {
	StructuredSubmission() bool
}

StructuredDriver is optional. A provider API that submits native fields (bodies, headers, attachments) rather than the rendered MIME document reports true, so Mailer skips MIME rendering; Outbound.MIME is then empty. SMTP, SES, Cloudflare, memory and log drivers receive the full MIME document.

type Template

type Template[T any] struct {
	// contains filtered or unexported fields
}

func NewTemplate

func NewTemplate[T any](source TemplateSource) (Template[T], error)

func (Template[T]) Render

func (t Template[T]) Render(ctx context.Context, data T) (Body, error)

type TemplateSource

type TemplateSource struct {
	Subject, Text, HTML string
	Layout              Layout
	MaxBytes            int
}

TemplateSource is trusted application template code. Data is escaped by html/template. Do not put untrusted template source or template.HTML in DTOs. Layout optionally wraps the Text and HTML bodies.

type UploadSource

type UploadSource = upload.Source

UploadSource accepts a captured browser upload (http.UploadedFile) without email depending on HTTP. The caller owns the underlying file.

Directories

Path Synopsis
Package cloudflare submits MIME through Cloudflare Email Service's REST API.
Package cloudflare submits MIME through Cloudflare Email Service's REST API.
Package failover composes borrowed email transports.
Package failover composes borrowed email transports.
internal
httptransport
Package httptransport owns the shared one-request, bounded-response policy.
Package httptransport owns the shared one-request, bounded-response policy.
Package log simulates acceptance while logging only bounded message metadata.
Package log simulates acceptance while logging only bounded message metadata.
Package mailgun submits multipart messages through Mailgun's v3 API.
Package mailgun submits multipart messages through Mailgun's v3 API.
Package memory supplies a bounded, opt-in email recorder.
Package memory supplies a bounded, opt-in email recorder.
Package postmark submits through Postmark's single-email API.
Package postmark submits through Postmark's single-email API.
Package resend submits through Resend's single-email HTTP API.
Package resend submits through Resend's single-email HTTP API.
Package ses submits MIME through SES SendRawEmail using the existing AWS SDK credential and SigV4 interfaces.
Package ses submits MIME through SES SendRawEmail using the existing AWS SDK credential and SigV4 interfaces.
Package smtp sends MIME messages over SMTP connections owned by the driver.
Package smtp sends MIME messages over SMTP connections owned by the driver.

Jump to

Keyboard shortcuts

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