email

package
v1.7.2 Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package email provides a provider-agnostic email transport layer.

The package exposes a small Transport interface that can be implemented by any backend. Out of the box it ships with:

  • SMTP transport (works with Gmail, AWS SES, SendGrid, Mailgun, Postmark, and any other provider that exposes SMTP).
  • Chain transport that tries multiple transports in order, providing primary/secondary/tertiary failover.
  • LogOnly transport for local development when no SMTP is configured; it emits a structured WARN log with the recipient and subject so the developer can manually deliver the message.

A small templates sub-package embeds plain text+HTML templates for common transactional emails (password reset, email verification, invitations).

Index

Constants

View Source
const (
	TemplatePasswordReset     = "password_reset"
	TemplateEmailVerification = "email_verification"
	TemplateInvitation        = "invitation"
	// TemplateTenantInvitation invites a recipient to join a redesign Tenant
	// (the company-governance entity), distinct from the admin-provisioning
	// TemplateInvitation. Carries the tenant name, the role offered, and the
	// raw-token acceptance link.
	TemplateTenantInvitation = "tenant_invitation"
	// TemplateEmailChangeVerify is sent to the *new* address with the
	// verification link to confirm the email change.
	TemplateEmailChangeVerify = "email_change_verify"
	// TemplateEmailChangeNotice is sent to the *old* address as a
	// security notice that an email change has been requested.
	TemplateEmailChangeNotice = "email_change_notice"
	// TemplateEmailLoginCode carries the 6-digit OTP for passwordless
	// email login.
	TemplateEmailLoginCode = "email_login_code"
	// TemplateMagicLink carries the clickable single-use sign-in link for
	// passwordless email login.
	TemplateMagicLink = "magic_link"
)

Known template names. Keep this list in sync with templates/.

Variables

View Source
var (
	// ErrInvalidMessage is returned (wrapped) when a Message fails validation.
	ErrInvalidMessage = errors.New("email: invalid message")

	// ErrTransport is returned (wrapped) when an underlying transport fails.
	ErrTransport = errors.New("email: transport failure")
)

Sentinel errors returned by the email package. Underlying errors are wrapped with %w so callers can use errors.Is to test against these sentinels.

Functions

func Redact added in v0.6.7

func Redact(s string) string

Redact returns a privacy-safe representation of an email address for use in logs and metrics labels: the first character of the local part, asterisks for the rest, and the domain. "alice@example.com" becomes "a***@example.com". Inputs without an "@" are returned unchanged.

Logs at production scale store every "to" address indefinitely; raw emails in those streams put the service squarely on the GDPR fast-lane. Every code path that logs an email address MUST use Redact instead.

func Render

func Render(name string, data any) (html, text string, err error)

Render returns the HTML and plain-text bodies for the given template name, with data substituted. The HTML side uses html/template (auto-escaping); the text side uses text/template.

Returns a wrapped error if the template is unknown or if execution fails.

Types

type Chain

type Chain struct {

	// OnAttempt, if non-nil, is invoked synchronously after each inner Send
	// with the index of the transport and the error it returned (nil on
	// success). Hook is intended for metrics; keep it cheap and non-blocking.
	OnAttempt func(idx int, err error)
	// contains filtered or unexported fields
}

Chain is a Transport that delegates to an ordered list of inner transports, trying each in turn until one succeeds. Use it for primary/secondary/tertiary failover across providers.

func NewChain

func NewChain(logger *zap.Logger, transports ...Transport) *Chain

NewChain builds a Chain. Returns an error-on-Send transport if transports is empty (rather than panicking) so callers can construct a Chain from config without special-casing the zero-provider path.

func (*Chain) Send

func (c *Chain) Send(ctx context.Context, m Message) error

Send tries each inner transport in order. Returns nil on first success. If all fail, returns the last error wrapped with ErrTransport.

type Message

type Message struct {
	// To is the recipient address (RFC 5322). Exactly one recipient is
	// supported; for multi-recipient sends, call Send once per recipient so
	// each delivery can be tracked and retried independently.
	To string

	// From is the sender address (RFC 5322). May be left empty when using a
	// transport that injects a default From (e.g. SMTPConfig.From).
	From string

	// Subject is the email subject line.
	Subject string

	// HTML is the HTML body. Optional if Text is set.
	HTML string

	// Text is the plain-text body. Optional if HTML is set.
	Text string

	// ReplyTo, when non-empty, is written as the Reply-To header (RFC 5322).
	// Used to route replies to a product support address while keeping the
	// transactional From address. Must parse as an address when set.
	ReplyTo string

	// ListUnsubscribe, when non-empty, is written verbatim as the
	// List-Unsubscribe header (RFC 2369), e.g.
	// "<mailto:unsubscribe@example.com>" or an https URL in angle brackets.
	// Improves deliverability/compliance; auth mail stays deliverable when
	// unset.
	ListUnsubscribe string
}

Message is a single outgoing email. Validate must be called (or NewMessage used) before passing to a Transport. Either HTML or Text (or both) must be non-empty.

func NewMessage

func NewMessage(to, from, subject, html, text string) (Message, error)

NewMessage constructs a Message and runs Validate. Returns a wrapped ErrInvalidMessage on failure.

func (Message) Validate

func (m Message) Validate() error

Validate checks the message is well-formed.

Rules:

  • To must parse as an RFC 5322 address.
  • From, if non-empty, must parse as an RFC 5322 address.
  • Subject must be non-empty.
  • At least one of HTML or Text must be non-empty.

type SMTPConfig

type SMTPConfig struct {
	// Host is the SMTP server hostname (e.g. "smtp.gmail.com").
	Host string

	// Port is the SMTP server port. Common values:
	//   25  - plain (dev / on-prem only).
	//   587 - submission with STARTTLS upgrade.
	//   465 - implicit TLS (a.k.a. SMTPS).
	Port int

	// User and Pass are credentials for PLAIN auth. If User is empty, no auth
	// is attempted.
	User string
	Pass string

	// From is the default From address used when Message.From is empty.
	From string

	// TLS enables implicit TLS on dial (use with port 465).
	TLS bool

	// StartTLS issues a STARTTLS upgrade after EHLO (use with port 587).
	StartTLS bool

	// InsecureSkipVerify disables TLS certificate verification. ONLY use this
	// in tests against self-signed servers; never enable it in production.
	InsecureSkipVerify bool

	// DialTimeout caps how long Dial may block. Defaults to 10s.
	DialTimeout time.Duration
}

SMTPConfig configures an SMTP transport. The zero value is not valid; use NewSMTP.

type Transport

type Transport interface {
	// Send delivers m. It must validate m and return a wrapped ErrInvalidMessage
	// for malformed inputs, or a wrapped ErrTransport (or other error) for
	// backend failures.
	Send(ctx context.Context, m Message) error
}

Transport is the abstraction over an email backend. Implementations must be safe for concurrent use and should honor ctx cancellation as best they can.

func NewLogOnly

func NewLogOnly(logger *zap.Logger) Transport

NewLogOnly returns a Transport that logs each Send at WARN and returns nil. Intended for local development when no SMTP server is configured.

func NewSMTP

func NewSMTP(cfg SMTPConfig) (Transport, error)

NewSMTP builds an SMTP transport from cfg.

Jump to

Keyboard shortcuts

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