email

package
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 25 Imported by: 0

README

clients/email

Pluggable transactional-email kit. Один Sender interface фронтит SMTP, AWS SES и Postmark backends — service-код не зависит от provider'а, ops переключают SMTP→SES одним env-флагом.

Импорт: github.com/theizzatbek/gokit/clients/email Зависит от: aws-sdk-go-v2/service/sesv2 (для SES) + errs + prometheus/client_golang

Quickstart

import "github.com/theizzatbek/gokit/clients/email"

sender, _ := email.New(email.Config{
    Backend: "smtp",
    SMTP: email.SMTPConfig{
        Host: "smtp.sendgrid.net", Port: 587,
        Username: "apikey", Password: os.Getenv("SENDGRID_API_KEY"),
    },
}, email.WithLogger(svc.Logger()), email.WithMetrics(reg))

err := sender.Send(ctx, email.Message{
    From: email.Address{Email: "no-reply@app.io", Name: "App"},
    To:   []email.Address{{Email: "alice@example.com"}},
    Subject:  "Welcome",
    HTMLBody: "<h1>Hi Alice!</h1>",
    TextBody: "Hi Alice!",
})

Backends

Backend Config-блок Заметки
smtp SMTPConfig stdlib net/smtp + STARTTLS. Универсальный — работает с любым SMTP-relay'ем (SendGrid SMTP-mode, Mailgun, self-hosted).
ses SESConfig aws-sdk-go-v2/service/sesv2. Reuse'ит default-credential-chain (env, instance-profile, IRSA).
postmark PostmarkConfig HTTP API через net/http. Honours MessageStream (transactional vs broadcast).
stub In-memory capture для тестов. s.Sent() → captured-Message-list.

Message contract

Поле Required Заметки
From.Email RFC 5322-mailbox. Name — optional.
To / CC / BCC ≥1 хотя бы в одном По крайней мере один recipient.
Subject Non-empty.
HTMLBody ИЛИ TextBody ≥1 Можно оба → SMTP/SES шлют как multipart/alternative.
Headers Map → key: value RFC 5322-headers.
Attachments Stream через io.Reader; читается единожды at Send time.
Tag Backend-specific tracking tag (Postmark Tag, SES EmailTags, SMTP X-Tag).

Message.Validate() enforces контракт; Send валидирует автоматически.

Templates

Опционально:

ts := email.NewTemplates()
//go:embed templates/*
var tplFS embed.FS
_ = ts.LoadFS(tplFS, "templates")

var msg email.Message
_ = ts.Render("welcome", map[string]string{"Name": "Alice"}, &msg)
msg.From = email.Address{Email: "no-reply@app.io"}
msg.To = []email.Address{{Email: "alice@example.com"}}
msg.Subject = "Welcome"
_ = sender.Send(ctx, msg)

Convention: welcome.html.tmpl + welcome.txt.tmpl оба загружаются под именем welcome; missing-side даёт пустой body (Validate всё равно требует хотя бы один non-empty body).

Опции

Опция Заметки
WithLogger(*slog.Logger) Debug на successful Send, Warn на error.
WithMetrics(reg) email_send_total{backend, outcome}, email_send_duration_seconds{backend}.

Error-mapping

Случай *errs.Error
Empty Backend / unknown backend KindValidation, email_invalid_config
Message.Validate reject KindValidation, email_invalid_message
Backend send-error KindUnavailable, email_send_failed
Postmark 401/403 KindPermission, email_send_failed
Postmark 429 KindRateLimited, email_send_failed
Template not registered KindValidation, email_template_not_found
Template exec failure KindValidation, email_template_exec_failed

См. также

  • clients/httpc — Postmark можно переключить на kit'овский transport через PostmarkConfig.HTTPClient
  • db/jobs — типичный паттерн "запланировать email через 30 мин"

Documentation

Overview

Package email is a pluggable transactional-email kit. A single Sender interface fronts SMTP, AWS SES, and Postmark backends so service code never depends on the chosen provider; ops can switch SMTP → SES with one config flag.

sender, err := email.New(cfg, email.WithLogger(svc.Logger()),
    email.WithMetrics(svc.MetricsRegistry()))
err = sender.Send(ctx, email.Message{
    From: email.Address{Email: "no-reply@app.io"},
    To:   []email.Address{{Email: "alice@example.com"}},
    Subject:  "Welcome",
    HTMLBody: "<h1>Hi Alice!</h1>",
    TextBody: "Hi Alice!",
})

Backends:

  • Backend "smtp" — net/smtp via STARTTLS. Stdlib-only.
  • Backend "ses" — AWS SES v2 (sesv2.SendEmail). Reuses aws-sdk-go-v2 config; honours IRSA / static creds.
  • Backend "postmark" — Postmark HTTP API via clients/httpc. Uses X-Postmark-Server-Token auth.
  • Backend "stub" — captures into Sent so tests can assert on outbound messages without a live transport.

Templates: optional html/template + text/template rendering via Templates.Render. Templates live as text files on disk; pass a glob or fs.FS to [LoadTemplates]. Kept lightweight — the kit is a transport, not a campaign tool.

Index

Constants

View Source
const (
	// CodeInvalidConfig — New received an empty/unknown Backend, or
	// the backend's required fields were missing.
	CodeInvalidConfig = "email_invalid_config"

	// CodeInvalidMessage — Send rejected a Message because of
	// missing From / no recipients / empty Subject AND body.
	CodeInvalidMessage = "email_invalid_message"

	// CodeSendFailed — backend reported an error. The original
	// SDK / SMTP / HTTP error is wrapped in Cause.
	CodeSendFailed = "email_send_failed"

	// CodeTemplateNotFound — Templates.Render was called with an
	// unknown template name.
	CodeTemplateNotFound = "email_template_not_found"

	// CodeTemplateExecFailed — html/text template Execute returned
	// an error. Usually a missing field on the supplied data.
	CodeTemplateExecFailed = "email_template_exec_failed"
)

Stable error Code constants produced by the package.

Variables

This section is empty.

Functions

This section is empty.

Types

type Address

type Address struct {
	Name  string
	Email string
}

Address is one mailbox. Name is optional (used in From-header formatting); Email is required.

func (Address) String

func (a Address) String() string

String returns the RFC 5322 mailbox formatting: `Name <email>` when Name is set, or bare email otherwise. Used by SMTP/SES headers.

type Attachment

type Attachment struct {
	Filename    string
	ContentType string // application/pdf, image/png, …
	Data        io.Reader
}

Attachment is one file attached to a Message. Data is read once at Send time (the io.Reader is fully consumed) — Reader implementations that don't support replay can't be reused across sends.

type Config

type Config struct {
	Backend string `env:"BACKEND"` // smtp|ses|postmark|stub

	SMTP     SMTPConfig
	SES      SESConfig
	Postmark PostmarkConfig
}

Config picks the backend + carries backend-specific knobs. Exactly one of SMTP / SES / Postmark must be set when Backend is the matching value.

type Message

type Message struct {
	From        Address
	To          []Address
	CC          []Address
	BCC         []Address
	ReplyTo     []Address
	Subject     string
	HTMLBody    string
	TextBody    string
	Headers     map[string]string
	Attachments []Attachment

	// Tag is an opt-in tracking label propagated to backends that
	// support it (Postmark X-PM-Message-Stream / SES Tags). Maps
	// to nothing on plain SMTP.
	Tag string
}

Message is the wire-shape Sender.Send consumes. The minimal valid shape is From + (≥1 of To/CC/BCC) + Subject + (HTMLBody OR TextBody). Validate enforces the contract.

func (Message) AllRecipients

func (m Message) AllRecipients() []string

AllRecipients returns the flat To+CC+BCC slice. Used by SMTP backends that need the envelope-RCPT-TO list.

func (Message) Validate

func (m Message) Validate() error

Validate reports the message-level contract:

  • From.Email non-empty
  • at least one recipient across To/CC/BCC
  • Subject non-empty
  • HTMLBody OR TextBody non-empty

Returns *errs.Error{Code: CodeInvalidMessage} on failure.

type Option

type Option func(*options)

Option tunes New.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger installs a *slog.Logger that receives Debug on success and Warn on send failures. nil silences output.

func WithMetrics

func WithMetrics(reg prometheus.Registerer) Option

WithMetrics registers Prometheus collectors:

  • email_send_total{backend, outcome}
  • email_send_duration_seconds{backend}

nil reg no-ops.

type PostmarkConfig

type PostmarkConfig struct {
	// ServerToken is the per-server token. Found under Postmark
	// → Server → API Tokens.
	ServerToken string `env:"SERVER_TOKEN"`

	// MessageStream picks the stream (default "outbound"). Postmark
	// requires a matching stream for broadcasts vs transactional.
	MessageStream string `env:"MESSAGE_STREAM" envDefault:"outbound"`

	// Endpoint overrides the default https://api.postmarkapp.com.
	// Used by tests + region-pinned EU deployments
	// (https://api.eu.postmarkapp.com).
	Endpoint string `env:"ENDPOINT"`

	// Timeout caps the round-trip per Send. Default 15s.
	Timeout time.Duration `env:"TIMEOUT" envDefault:"15s"`

	// HTTPClient is an optional override. Set to wire kit's
	// clients/httpc retries / metrics. nil falls back to a
	// fresh net/http.DefaultClient with Timeout.
	HTTPClient *http.Client
}

PostmarkConfig is the backend-specific block for Config when Backend == "postmark". ServerToken is required.

type SESConfig

type SESConfig struct {
	Region          string `env:"REGION" envDefault:"us-east-1"`
	AccessKeyID     string `env:"ACCESS_KEY_ID"`
	SecretAccessKey string `env:"SECRET_ACCESS_KEY"`
	SessionToken    string `env:"SESSION_TOKEN"`

	// ConfigurationSet activates per-set SES features (event
	// publishing to SNS, IP-pool routing). Empty leaves the default.
	ConfigurationSet string `env:"CONFIGURATION_SET"`

	// Endpoint overrides the default service endpoint (LocalStack /
	// kit tests). Empty uses AWS resolved endpoint.
	Endpoint string `env:"ENDPOINT"`

	// Timeout caps the round-trip per Send. Default 15s.
	Timeout time.Duration `env:"TIMEOUT" envDefault:"15s"`
}

SESConfig is the backend-specific block for Config when Backend == "ses". All fields are optional — empty AccessKeyID + SecretAccessKey falls back to the SDK default credential chain (env, instance-profile, IRSA).

type SMTPConfig

type SMTPConfig struct {
	Host     string `env:"HOST"`
	Port     int    `env:"PORT" envDefault:"587"`
	Username string `env:"USERNAME"`
	Password string `env:"PASSWORD"`

	// AuthMethod is the credential mechanism used. "plain" (default)
	// runs PLAIN over STARTTLS; "cram-md5" uses CRAM-MD5. Empty
	// Username disables auth entirely (no-auth relay).
	AuthMethod string `env:"AUTH_METHOD"`

	// LocalName is the HELO/EHLO domain. Empty defaults to
	// "localhost"; set to your sending host for IP-based reputation
	// systems that match HELO against rDNS.
	LocalName string `env:"LOCAL_NAME"`

	// StartTLS toggles STARTTLS upgrade. true (default) is correct
	// for port 587; set false ONLY for port 25 to local mail-relay
	// inside trusted networks.
	StartTLS bool `env:"START_TLS" envDefault:"true"`

	// Timeout caps the total round-trip per Send. Default 30s.
	Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
}

SMTPConfig is the backend-specific block for Config when Backend == "smtp". Host + Port are required.

type Sender

type Sender interface {
	Send(ctx context.Context, msg Message) error
}

Sender is the abstract contract every backend satisfies. Goroutine-safe — implementations protect their own state.

func New

func New(cfg Config, opts ...Option) (Sender, error)

New constructs a Sender bound to the selected backend. Returns *errs.Error{Code: CodeInvalidConfig} for an unknown Backend.

Logger + metrics are best-effort: nil values silently skip observability.

type Stub

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

Stub is an in-memory Sender for tests / dev / staging where real delivery is undesirable. Captures every successful Send into Sent so test code can assert. Goroutine-safe.

func NewStub

func NewStub() *Stub

NewStub returns a fresh Stub.

func (*Stub) Reset

func (s *Stub) Reset()

Reset clears the captured-Sent list.

func (*Stub) Send

func (s *Stub) Send(_ context.Context, msg Message) error

Send validates msg and appends to Sent. Returns the validation error from Message.Validate.

func (*Stub) Sent

func (s *Stub) Sent() []Message

Sent returns a snapshot copy of every Message successfully Send'd.

type Templates

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

Templates is a thread-safe template registry. Stores parsed html/template + text/template pairs keyed by base name. Use Templates.Render to fill a Message's HTMLBody / TextBody from data.

Convention: a template named "welcome" loads from "welcome.html.tmpl" + "welcome.txt.tmpl" (either OR both). Missing halves render to empty body; Send still validates that at least one body part is non-empty.

func NewTemplates

func NewTemplates() *Templates

NewTemplates returns an empty registry. Populate via [LoadGlob] or [LoadFS].

func (*Templates) LoadFS

func (t *Templates) LoadFS(fsys fs.FS, root string) error

LoadFS loads every matching template from an fs.FS (embed.FS-friendly).

func (*Templates) LoadGlob

func (t *Templates) LoadGlob(pat string) error

LoadGlob loads every `*.html.tmpl` and `*.txt.tmpl` matched by pat (forwarded to filepath.Glob). Base name is the file stem with the `.html` / `.txt` suffix stripped, e.g. `welcome.html.tmpl` → `welcome`. Subsequent loads for the same name overwrite the prior parse (test-friendly).

func (*Templates) Render

func (t *Templates) Render(name string, data any, msg *Message) error

Render fills msg.HTMLBody and msg.TextBody from the templates registered under name. Either side may be empty if the matching `*.html.tmpl` / `*.txt.tmpl` wasn't loaded — Send's Validate still requires at least one populated body, so callers should ship at least one half per template.

Returns *errs.Error{Code: CodeTemplateNotFound} when neither the html- nor text-half is registered.

Jump to

Keyboard shortcuts

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