email

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 13 Imported by: 0

README

go-email

A small, dependency-light email client for Go: a full RFC 5322 envelope, multipart (alternative/mixed/related) rendering, and an ordered middleware chain around a pluggable Transport.

The exported surface is a set of neutral interfaces (Transport, Renderer, Sender), so a caller never has to import net/smtp or mime/multipart directly. The core package stays dependency-free and telemetry-free; SMTP delivery, templated rendering, and OpenTelemetry instrumentation each live in their own subpackage.

Install

go get github.com/Bugs5382/go-email

Usage

renderer := template.New()
renderer.Register(
	"welcome",
	"Welcome, {{.Name}}!",
	"<p>Hi {{.Name}}, welcome aboard.</p>",
	"Hi {{.Name}}, welcome aboard.",
)

sender := email.New(
	smtp.NewSMTPTransport(smtp.LoadConfig()),
	email.WithMiddleware(email.Validate(), email.Retry(3, time.Second)),
	email.WithRenderer(renderer),
)

err := sender.SendKind(ctx, "welcome", email.Message{
	From: "no-reply@example.com",
	To:   []string{"user@example.com"},
}, map[string]any{"Name": "Ada"})

smtp.LoadConfig reads SMTP_* environment variables with defaults suited to a local mail catcher (host localhost, port 1025, no auth, no TLS) -- point it at a real relay by setting SMTP_HOST/SMTP_PORT/SMTP_USER/ SMTP_PASS/SMTP_TLS in production. See example_test.go for a complete, compilable example.

Envelope

Message is the neutral, transport-agnostic envelope: From/To/Cc/Bcc, ReplyTo, Subject/HTML/Text, Attachments (including inline attachments referenced via cid:), arbitrary Headers, Priority and Sensitivity hints, List-Unsubscribe/List-Unsubscribe-Post, and a Meta map for middleware/subpackage use (e.g. tracing attributes).

  • When both HTML and Text are set, the message renders as multipart/alternative -- the plaintext body is a first-class fallback, not an afterthought.
  • Bcc recipients are only ever passed to the transport's envelope (SMTP RCPT TO); they are never written into a message header.

Middleware and hooks

Sender runs every Message through an ordered middleware chain before handing it to the Transport:

  • Validate() -- rejects a Message missing a From or any recipient.
  • Retry(attempts, base) -- retries a failed Send with exponential backoff.
  • Dedupe(Deduper) -- skips a Message already seen by a caller-supplied Deduper (an in-memory MemDeduper is included).
  • Record(Recorder) -- reports every send attempt, success or failure, to a caller-supplied Recorder.
  • Suppress(Suppressor) -- skips recipients on a caller-supplied suppression list, returning ErrSuppressed when every recipient is suppressed.
  • Sign(Signer) / Encrypt(Encryptor) -- hook seams for a caller-supplied S/MIME, PGP, or other signing/encryption implementation.

SendBulk sends one rendered Message per recipient through the same middleware chain, with per-recipient throttling and a BulkResult tally instead of aborting the batch on the first failure.

Subpackages

  • smtp -- Config/LoadConfig and an SMTPTransport speaking net/smtp, with a plaintext no-auth path for local catchers (e.g. maildev) and an optional STARTTLS+auth relay path.
  • template -- a TemplateRenderer backed by the standard library's text/template (subject, plaintext) and html/template (HTML, auto-escaped) packages.
  • otel -- an email.Middleware that wraps every Send in an OpenTelemetry span plus send-count and duration metrics. It is the only package in this module that imports go.opentelemetry.io/otel; the core package stays telemetry-free.

Develop

task build    # go build ./...
task test     # go test ./...
task lint     # gofmt check + golangci-lint + yamllint
task ci       # build + vet + lint
task license  # verify every source file carries the MIT header

License

MIT © 2026 Shane

Documentation

Overview

Package email is a small, dependency-light email client: a full RFC 5322 envelope, multipart (alternative/mixed/related) rendering, and a middleware hook chain around a pluggable Transport. The default SMTPTransport speaks net/smtp with an optional STARTTLS+auth relay path and a plaintext no-auth path for local catchers.

Neutral interfaces (Transport, Renderer, Sender) keep the implementation (net/smtp, html/template) out of the exported surface, so consumers wrap or depend on the interface without importing those packages.

Example

Example shows the common wiring: an SMTP transport, a Validate+Retry middleware chain, and a template.Renderer, composed into a Sender that delivers a templated message by kind.

It has no "Output:" comment, so `go test` compiles it but does not run it -- SendKind below would otherwise dial the SMTP host from smtp.LoadConfig (a local catcher such as maildev by default), which this package's test suite must not depend on.

package main

import (
	"context"
	"fmt"
	"time"

	email "github.com/Bugs5382/go-email"
	"github.com/Bugs5382/go-email/smtp"
	"github.com/Bugs5382/go-email/template"
)

func main() {
	renderer := template.New()
	if err := renderer.Register(
		"welcome",
		"Welcome, {{.Name}}!",
		"<p>Hi {{.Name}}, welcome aboard.</p>",
		"Hi {{.Name}}, welcome aboard.",
	); err != nil {
		fmt.Println(err)
		return
	}

	sender := email.New(
		smtp.NewSMTPTransport(smtp.LoadConfig()),
		email.WithMiddleware(email.Validate(), email.Retry(3, time.Second)),
		email.WithRenderer(renderer),
	)

	ctx := context.Background()
	msg := email.Message{
		From: "no-reply@example.com",
		To:   []string{"user@example.com"},
	}

	if err := sender.SendKind(ctx, "welcome", msg, map[string]any{"Name": "Ada"}); err != nil {
		fmt.Println(err)
	}
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoRenderer = errors.New("email: SendKind requires a Renderer (see WithRenderer)")

ErrNoRenderer is returned by SendKind when the Sender was built without WithRenderer.

View Source
var ErrSuppressed = errors.New("email: all recipients suppressed")

ErrSuppressed is returned by the Suppress middleware when every recipient (To ∪ Cc ∪ Bcc) has been filtered out as suppressed, so there is no one left to send to. Suppress returns it in place of calling next, giving callers (e.g. SendBulk) a distinguishable, explicit outcome instead of having to infer a skip from an emptied recipient list.

View Source
var ErrValidation = errors.New("email: message failed validation")

ErrValidation is returned (wrapped) by Validate when a Message fails envelope validation. Callers can test for it with errors.Is.

Functions

This section is empty.

Types

type Attachment

type Attachment struct {
	Filename    string
	ContentType string
	Content     []byte
	Inline      bool
	ContentID   string
}

Attachment is a single file attached to, or inlined within, a Message. Inline attachments (Inline true) are referenced from HTML bodies via ContentID, e.g. `<img src="cid:ContentID">`.

type BulkOption

type BulkOption func(*bulkConfig)

BulkOption configures a SendBulk call.

func WithListUnsubscribe

func WithListUnsubscribe(url, post string) BulkOption

WithListUnsubscribe sets the List-Unsubscribe and List-Unsubscribe-Post header values applied to every recipient's Message in a bulk send.

func WithThrottle

func WithThrottle(rate time.Duration) BulkOption

WithThrottle paces a bulk send by waiting rate between each recipient's send. A non-positive rate disables throttling (the default).

type BulkResult

type BulkResult struct {
	Sent, Skipped, Failed int
	Errors                map[string]error
}

BulkResult tallies the outcome of a SendBulk call: how many recipients were actually sent to, how many were skipped (e.g. suppressed), how many failed, and the per-address error for each failure.

type Deduper

type Deduper interface {
	// Seen reports whether key has already been marked as sent.
	Seen(ctx context.Context, key string) (bool, error)
	// Mark records key as sent.
	Mark(ctx context.Context, key string) error
}

Deduper decides whether a Message identified by an opaque dedup key has already been sent, and records that a key has now been sent. Consumers derive the key however they see fit (e.g. a template name plus recipient) and place it in Message.Meta["dedup_key"].

type Encryptor

type Encryptor interface {
	Encrypt(ctx context.Context, m *Message) error
}

Encryptor encrypts m (or parts of it) before it is sent.

type MemDeduper

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

MemDeduper is an in-memory, mutex-guarded Deduper suitable for a single process or for tests. It does not persist across restarts and does not coordinate across processes; long-lived or multi-process deployments should supply their own Deduper backed by shared storage.

func NewMemDeduper

func NewMemDeduper() *MemDeduper

NewMemDeduper returns a ready-to-use MemDeduper.

func (*MemDeduper) Mark

func (d *MemDeduper) Mark(_ context.Context, key string) error

Mark implements Deduper.

func (*MemDeduper) Seen

func (d *MemDeduper) Seen(_ context.Context, key string) (bool, error)

Seen implements Deduper.

type Message

type Message struct {
	From, EnvelopeFrom string

	To, Cc, Bcc []string

	ReplyTo, Subject, HTML, Text string

	Attachments []Attachment
	Headers     map[string]string

	Priority    Priority
	Sensitivity Sensitivity

	ListUnsubscribe, ListUnsubscribePost string

	Meta map[string]any
}

Message is the neutral, transport-agnostic email envelope. It carries no dependency on any concrete transport (e.g. net/smtp) so callers can build and inspect a Message without pulling in an implementation.

func (Message) Bytes

func (m Message) Bytes() ([]byte, error)

Bytes renders m into RFC 5322 message bytes (headers plus body), ready to be handed to a Transport for delivery. See buildMIME for the MIME structure it produces.

func (Message) Recipients

func (m Message) Recipients() []string

Recipients returns the full SMTP RCPT TO set: To ∪ Cc ∪ Bcc.

type Middleware

type Middleware func(next SendFunc) SendFunc

Middleware wraps a SendFunc with additional behavior, returning a new SendFunc that runs that behavior around a call to next.

func Dedupe

func Dedupe(d Deduper) Middleware

Dedupe returns a Middleware that skips sending a Message whose Meta["dedup_key"] has already been marked as seen by d, unless Meta["resend"] is true. Messages without a dedup key always send. On a successful send, the key is marked so future attempts are deduplicated.

func Encrypt

func Encrypt(e Encryptor) Middleware

Encrypt returns a Middleware that calls e.Encrypt on m before next. If e is nil, the returned Middleware is the identity: it calls next unchanged.

func Record

func Record(r Recorder) Middleware

Record returns a Middleware that calls next and then reports the outcome (nil or non-nil) to r, including failures. The original send error (if any) is always returned to the caller, regardless of what r.Record returns.

func Retry

func Retry(attempts int, base time.Duration) Middleware

Retry returns a Middleware that retries next up to attempts times when it fails with a TransientError, using exponential backoff starting at base (base, base*2, base*4, ...) between attempts. Non-transient errors are returned immediately without retrying. The wait between attempts honors ctx cancellation. An attempts value below 1 is treated as 1, so next is always called at least once rather than being silently skipped.

func Sign

func Sign(s Signer) Middleware

Sign returns a Middleware that calls s.Sign on m before next. If s is nil, the returned Middleware is the identity: it calls next unchanged.

func Suppress

func Suppress(s Suppressor) Middleware

Suppress returns a Middleware that removes addresses reported as suppressed by s from To, Cc, and Bcc before calling next. It is typically placed ahead of bulk sends to honor bounce or unsubscribe lists. If filtering leaves no recipient at all, it returns ErrSuppressed without calling next, rather than attempting a send with zero recipients.

func Validate

func Validate() Middleware

Validate returns a Middleware that rejects a Message before it reaches the next stage unless: From is a syntactically valid address, there is at least one recipient (To ∪ Cc ∪ Bcc), and every recipient address is syntactically valid. Failures are reported as ErrValidation (wrapped with details via errors.Is-compatible wrapping).

type NopDeduper

type NopDeduper struct{}

NopDeduper is a Deduper that never remembers anything: every key is reported as unseen, and Mark is a no-op. It is the zero-value default for consumers that do not need deduplication.

func (NopDeduper) Mark

Mark is a no-op.

func (NopDeduper) Seen

Seen always reports false.

type NopRecorder

type NopRecorder struct{}

NopRecorder is a Recorder that discards every outcome. It is the zero-value default for consumers that do not need send auditing.

func (NopRecorder) Record

Record is a no-op.

type NopSuppressor

type NopSuppressor struct{}

NopSuppressor is a Suppressor that never suppresses any address. It is the zero-value default for consumers that do not maintain a suppression list.

func (NopSuppressor) Suppressed

func (NopSuppressor) Suppressed(context.Context, string) (bool, error)

Suppressed always reports false.

type Option

type Option func(*sender)

Option configures a Sender built by New.

func WithMiddleware

func WithMiddleware(mws ...Middleware) Option

WithMiddleware appends mws, in order, to the Sender's middleware chain. mws[0] runs outermost (first), mirroring chain's semantics.

func WithRenderer

func WithRenderer(r Renderer) Option

WithRenderer sets the Renderer that SendKind uses to resolve a kind and data into a Message body. Without it, SendKind returns an error.

type Priority

type Priority int

Priority is the RFC 2076-family email priority hint (Importance/X-Priority/ Priority headers). The zero value, PriorityNormal, emits no priority headers at all.

const (
	PriorityNormal Priority = iota
	PriorityHigh
	PriorityLow
)

type Recipient

type Recipient struct {
	Address string
	Data    any
}

Recipient is one target of a bulk send: an address plus the data a Renderer resolves into that recipient's Subject/HTML/Text body.

type Recorder

type Recorder interface {
	Record(ctx context.Context, m *Message, sendErr error) error
}

Recorder observes the outcome of every send attempt, whether it succeeded or failed. It is typically used for audit trails or delivery logs.

type Rendered

type Rendered struct {
	Subject, HTML, Text string
}

Rendered is the resolved subject/HTML/text content produced by a Renderer, ready to be assembled into a Message body.

type Renderer

type Renderer interface {
	Render(ctx context.Context, kind string, data any) (Rendered, error)
}

Renderer resolves a subject/HTML/text body for a named kind of content (e.g. a template name) and arbitrary data, returning it ready to be assembled into a Message. No concrete implementation (e.g. html/template) appears in this interface, so callers can depend on Renderer without pulling in one.

type SendFunc

type SendFunc func(ctx context.Context, m *Message) error

SendFunc sends a single Message. It is the terminal operation that a Middleware chain wraps: the innermost SendFunc typically delegates to a Transport, while each Middleware layer adds a cross-cutting concern (validation, retries, auditing, and so on) around it.

type Sender

type Sender interface {
	// Send runs m through the middleware chain and delivers it via the
	// underlying Transport.
	Send(ctx context.Context, m Message) error
	// SendKind resolves kind and data via the configured Renderer into a
	// Subject/HTML/Text body, applies it to a copy of m, and sends that
	// copy via Send.
	SendKind(ctx context.Context, kind string, m Message, data any) error
	// SendBulk resolves kind and each Recipient's Data via the configured
	// Renderer into a copy of base addressed to that one recipient, and
	// sends every copy through the same path Send uses. A per-recipient
	// failure or suppression is tallied in the returned BulkResult rather
	// than aborting the rest of the batch.
	SendBulk(ctx context.Context, kind string, base Message, recipients []Recipient, opts ...BulkOption) (BulkResult, error)
}

Sender is the top-level entry point for delivering mail: it runs a Message through the configured middleware chain to a Transport, optionally resolving its content from a named kind via a Renderer first.

func New

func New(t Transport, opts ...Option) Sender

New builds a Sender that delivers via t, running every Message through the middleware chain assembled from opts (outermost first) before t.Send sees it.

type Sensitivity

type Sensitivity int

Sensitivity is the RFC 5322 Sensitivity header hint. The zero value, SensitivityNormal, emits no Sensitivity header.

const (
	SensitivityNormal Sensitivity = iota
	SensitivityPersonal
	SensitivityPrivate
	SensitivityConfidential
)

type Signer

type Signer interface {
	Sign(ctx context.Context, m *Message) error
}

Signer applies a message signature (e.g. DKIM) to m before it is sent.

type Suppressor

type Suppressor interface {
	Suppressed(ctx context.Context, addr string) (bool, error)
}

Suppressor decides whether a given address must never receive mail (e.g. a bounce or unsubscribe list).

type TransientError

type TransientError struct {
	Err error
}

TransientError marks an error as transient: safe to retry. Transports and other middleware should wrap retryable failures (e.g. temporary network or SMTP 4xx errors) in a TransientError so that Retry knows to act on them.

func (TransientError) Error

func (e TransientError) Error() string

Error implements the error interface.

func (TransientError) Unwrap

func (e TransientError) Unwrap() error

Unwrap allows errors.Is/errors.As to see through to the wrapped error.

type Transport

type Transport interface {
	Send(ctx context.Context, m Message) error
}

Transport delivers a Message. It is the neutral seam between the envelope/rendering layer and the wire protocol: no concrete implementation (e.g. net/smtp) type appears in this interface, so callers can depend on Transport without pulling in one.

Directories

Path Synopsis
Package otel provides an OpenTelemetry email.Middleware: it wraps a Send call in a span plus send counter and duration metrics.
Package otel provides an OpenTelemetry email.Middleware: it wraps a Send call in a span plus send counter and duration metrics.
Package template provides a TemplateRenderer, a concrete email.Renderer backed by the standard library's text/template and html/template packages.
Package template provides a TemplateRenderer, a concrete email.Renderer backed by the standard library's text/template and html/template packages.

Jump to

Keyboard shortcuts

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