Documentation
¶
Overview ¶
Package email provides production-ready email sending capabilities for Go applications.
This package offers a complete email solution with SMTP support, HTML and plain text templates, file attachments, retry logic, rate limiting, and comprehensive error handling. It is designed to be simple to use while providing the flexibility needed for production applications.
Features ¶
- SMTP sending with TLS/STARTTLS support
- HTML and plain text email bodies
- Go template engine integration for dynamic content
- File attachment support with proper MIME encoding
- Automatic retry with exponential backoff
- Built-in rate limiting
- Pluggable logging interface
- Context support for timeouts and cancellation
- Email address validation and header injection protection
- Mock sender for testing
- Batch sending with concurrency control
- SMTP connection pooling for high-throughput sending
- Composable middleware pipeline (logging, metrics, recovery, hooks)
- Async queue worker for non-blocking background delivery
- Provider adapters: SendGrid, Mailgun, AWS SES (providers submodules)
- DKIM signing with RSA-SHA256 and Ed25519-SHA256 (RFC 6376/8463)
- Minimal dependencies (only golang.org/x/sync and golang.org/x/time)
Quick Start ¶
Basic email sending:
config := email.SMTPConfig{
Host: "smtp.gmail.com",
Port: 587,
Username: "your-email@gmail.com",
Password: "your-app-password",
From: "your-email@gmail.com",
UseTLS: true,
}
sender, err := email.NewSMTPSender(config)
if err != nil {
log.Fatal(err)
}
mailer := email.NewMailer(sender, config.From)
defer mailer.Close()
ctx := context.Background()
err = mailer.Send(ctx,
[]string{"recipient@example.com"},
"Hello!",
"This is a test email.",
)
Using Templates ¶
Create and use email templates:
tmpl := email.NewTemplate("welcome")
tmpl.SetSubject("Welcome {{.Name}}!")
tmpl.SetHTMLTemplate(`
<h1>Hello {{.Name}}!</h1>
<p>Welcome to our service.</p>
`)
mailer.RegisterTemplate("welcome", tmpl)
data := map[string]any{"Name": "John Doe"}
err := mailer.SendTemplate(ctx, []string{"john@example.com"}, "welcome", data)
With Attachments ¶
Send emails with file attachments:
pdfData, err := os.ReadFile("document.pdf")
if err != nil {
log.Fatal(err)
}
email := email.NewEmail().
SetFrom("sender@example.com").
AddTo("recipient@example.com").
SetSubject("Document Attached").
SetBody("Please find the document attached.").
AddAttachment("document.pdf", "application/pdf", pdfData)
err := mailer.SendEmail(ctx, email)
Logging ¶
The package uses a simple Logger interface for observability. By default, no logs are produced. To enable logging, provide a Logger implementation in SMTPConfig:
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
config := email.SMTPConfig{
Host: "smtp.gmail.com",
Logger: email.NewSlogLogger(logger),
}
The package logs at these levels:
- Debug: Connection details, SMTP commands, retry attempts
- Info: Email sent/received events, configuration
- Warn: Retries, rate limiting, non-fatal issues
- Error: Send failures, validation errors, connection errors
Connection Pooling ¶
For high-throughput sending, enable SMTP connection pooling to reuse established connections across sends, avoiding per-email TCP + TLS + AUTH overhead:
config := email.SMTPConfig{
Host: "smtp.gmail.com",
Port: 587,
Username: "your-email@gmail.com",
Password: "your-app-password",
UseTLS: true,
PoolSize: 5, // Enable pooling with 5 max connections
}
sender, err := email.NewSMTPSender(config)
if err != nil {
log.Fatal(err)
}
defer sender.Close() // Important: closes all pooled connections
Pool configuration options:
- PoolSize: Max open connections (0 = disabled, default)
- MaxIdleConns: Max idle connections in pool (default: 2)
- PoolMaxLifetime: Max connection lifetime (default: 30m)
- PoolMaxIdleTime: Max idle time before eviction (default: 5m)
- MaxMessages: Max messages per connection before rotation (default: 100)
- PoolWaitTimeout: Max wait when pool is exhausted (default: 5s)
Middleware Pipeline ¶
The package supports composable middleware for cross-cutting concerns. Middleware wraps the Sender interface, following the same pattern as net/http middleware:
wrapped := email.Chain(sender,
email.WithRecovery(),
email.WithLogging(logger),
email.WithHooks(hooks),
email.WithMetrics(collector),
)
mailer := email.NewMailer(wrapped, "from@example.com")
Or use NewMailerWithOptions:
mailer := email.NewMailerWithOptions(sender, "from@example.com",
email.WithMiddleware(
email.WithRecovery(),
email.WithLogging(logger),
),
)
Built-in middlewares:
- WithLogging: logs send attempts and outcomes using the Logger interface
- WithRecovery: catches panics and converts them to errors
- WithHooks: invokes configurable OnSend/OnSuccess/OnFailure callbacks
- WithMetrics: records send metrics via the MetricsCollector interface
For OpenTelemetry tracing, see the providers/otelmail submodule (github.com/KARTIKrocks/goemail/providers/otelmail).
DKIM Signing ¶
Sign outgoing emails with DKIM-Signature headers for improved deliverability. Set DKIMConfig on SMTPConfig to automatically sign all outgoing SMTP messages:
privateKey, _ := email.ParseDKIMPrivateKey(pemData)
config := email.SMTPConfig{
Host: "smtp.example.com",
Port: 587,
DKIM: &email.DKIMConfig{
Domain: "example.com",
Selector: "default",
PrivateKey: privateKey,
},
}
For raw message signing (e.g., with AWS SES), use BuildRawMessageWithDKIM or call SignMessage directly. Supports RSA-SHA256 and Ed25519-SHA256 algorithms.
Async Sending ¶
Wrap any Sender with AsyncSender for non-blocking background delivery:
async := email.NewAsyncSender(sender,
email.WithQueueSize(200),
email.WithWorkers(3),
)
defer async.Close()
// Fire-and-forget
err := async.Send(ctx, myEmail)
// Or block until delivered
err = async.SendWait(ctx, myEmail)
AsyncSender implements the Sender interface and composes with middleware.
Provider Adapters ¶
Send via HTTP APIs instead of SMTP using providers submodules:
- providers/sendgrid — SendGrid v3 Web API
- providers/mailgun — Mailgun v3 Messages API
- providers/ses — AWS SES v2 with raw MIME messages
All adapters implement the Sender interface and live in separate Go modules.
Testing ¶
Use MockSender for testing:
mock := email.NewMockSender()
mailer := email.NewMailer(mock, "test@example.com")
err := mailer.Send(ctx, []string{"user@example.com"}, "Test", "Body")
// Verify
if mock.GetEmailCount() != 1 {
t.Error("expected 1 email")
}
Error Handling ¶
The package provides detailed errors with context:
err := mailer.Send(ctx, to, subject, body)
if err != nil {
var emailErr *email.Error
if errors.As(err, &emailErr) {
log.Printf("Operation: %s, From: %s, To: %v, Error: %v",
emailErr.Op, emailErr.From, emailErr.To, emailErr.Err)
}
}
Context Support ¶
All send operations accept context.Context for timeouts and cancellation:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := mailer.Send(ctx, to, subject, body)
if errors.Is(err, context.DeadlineExceeded) {
log.Println("send timed out")
}
Security ¶
The package includes several security features:
- Email address validation using net/mail
- Email header injection protection
- Automatic sanitization of headers
- TLS/STARTTLS support for encrypted connections
Always use environment variables for sensitive credentials:
config := email.SMTPConfig{
Host: os.Getenv("SMTP_HOST"),
Username: os.Getenv("SMTP_USERNAME"),
Password: os.Getenv("SMTP_PASSWORD"),
}
For Gmail, use App Passwords instead of your regular password. Enable 2FA and generate an App Password at: https://myaccount.google.com/apppasswords
Index ¶
- Constants
- Variables
- func BuildRawMessage(e *Email) ([]byte, error)
- func BuildRawMessageWithDKIM(e *Email, dkim *DKIMConfig) ([]byte, error)
- func LoadTemplatesFromDir(dir string, patterns ...string) (map[string]*Template, error)
- func LoadTemplatesFromFS(fsys fs.FS, patterns ...string) (map[string]*Template, error)
- func ParseDKIMPrivateKey(pemData []byte) (crypto.Signer, error)
- func SanitizeFuncMap() htmltemplate.FuncMap
- func SanitizeFuncMapWithPolicy(p *Policy) htmltemplate.FuncMap
- func SanitizeHTML(html string) string
- func SanitizeHTMLWithPolicy(html string, p *Policy) string
- func SignMessage(msg []byte, config *DKIMConfig) ([]byte, error)
- type AsyncOption
- type AsyncSender
- type Attachment
- type Canonicalization
- type DKIMConfig
- type Email
- func (e *Email) AddAttachment(filename, contentType string, data []byte) *Email
- func (e *Email) AddBcc(bcc ...string) *Email
- func (e *Email) AddCc(cc ...string) *Email
- func (e *Email) AddHeader(key, value string) *Email
- func (e *Email) AddTo(to ...string) *Email
- func (e *Email) Build() (*Email, error)
- func (e *Email) SetBody(body string) *Email
- func (e *Email) SetFrom(from string) *Email
- func (e *Email) SetHTMLBody(html string) *Email
- func (e *Email) SetReplyTo(replyTo string) *Email
- func (e *Email) SetSubject(subject string) *Email
- func (e *Email) Validate() error
- type Error
- type EventType
- type Logger
- type Mailer
- func (m *Mailer) Close() error
- func (m *Mailer) RegisterTemplate(name string, tmpl *Template)
- func (m *Mailer) Send(ctx context.Context, to []string, subject, body string) error
- func (m *Mailer) SendBatch(ctx context.Context, emails []*Email, concurrencyLimit int) error
- func (m *Mailer) SendEmail(ctx context.Context, email *Email) error
- func (m *Mailer) SendHTML(ctx context.Context, to []string, subject, html string) error
- func (m *Mailer) SendTemplate(ctx context.Context, to []string, templateName string, data any) error
- type MailerOption
- type MetricsCollector
- type Middleware
- type MockSender
- func (m *MockSender) Close() error
- func (m *MockSender) GetEmailCount() int
- func (m *MockSender) GetEmailsBySubject(subject string) []*Email
- func (m *MockSender) GetEmailsTo(recipient string) []*Email
- func (m *MockSender) GetLastEmail() *Email
- func (m *MockSender) GetSentEmails() []*Email
- func (m *MockSender) Reset()
- func (m *MockSender) Send(ctx context.Context, email *Email) error
- func (m *MockSender) SetSendFunc(fn func(ctx context.Context, email *Email) error)
- type NoOpLogger
- type NoOpMetricsCollector
- type Policy
- func (p *Policy) AllowAttributes(element string, attrs ...string) *Policy
- func (p *Policy) AllowElements(elements ...string) *Policy
- func (p *Policy) AllowGlobalAttributes(attrs ...string) *Policy
- func (p *Policy) AllowURLProtocols(attr string, protocols ...string) *Policy
- func (p *Policy) StripElements(elements ...string) *Policy
- type SMTPConfig
- type SMTPSender
- type SendHooks
- type Sender
- type SlogLogger
- type Template
- func (t *Template) Render(data any) (*Email, error)
- func (t *Template) SetHTMLTemplate(tmpl string) (*Template, error)
- func (t *Template) SetSubject(subject string) *Template
- func (t *Template) SetTextTemplate(tmpl string) (*Template, error)
- func (t *Template) WithSanitization() *Template
- func (t *Template) WithSanitizationPolicy(p *Policy) *Template
- type WebhookEvent
- type WebhookHandler
- type WebhookHandlerFunc
- type WebhookParser
- type WebhookReceiver
- type WebhookReceiverOption
Examples ¶
Constants ¶
const ( DefaultMaxIdleConns = 2 DefaultPoolMaxLifetime = 30 * time.Minute DefaultPoolMaxIdleTime = 5 * time.Minute DefaultMaxMessages = 100 DefaultPoolWaitTimeout = 5 * time.Second )
Pool default constants.
const ( // DefaultTimeout is the default timeout for SMTP operations DefaultTimeout = 30 * time.Second // DefaultMaxRetries is the default number of retry attempts DefaultMaxRetries = 3 // DefaultRetryDelay is the default initial retry delay DefaultRetryDelay = time.Second // DefaultRetryBackoff is the default exponential backoff multiplier DefaultRetryBackoff = 2.0 // DefaultRateLimit is the default rate limit (emails per second) DefaultRateLimit = 10 )
Variables ¶
var ( // ErrQueueFull is returned by AsyncSender.Send when the queue buffer is full. ErrQueueFull = errors.New("email: async queue is full") // ErrQueueClosed is returned by AsyncSender.Send after Close has been called. ErrQueueClosed = errors.New("email: async queue is closed") )
var ( // ErrNoRecipients is returned when no recipients are specified ErrNoRecipients = errors.New("email: no recipients specified") // ErrNoSender is returned when no sender is specified ErrNoSender = errors.New("email: no sender specified") // ErrNoSubject is returned when no subject is specified ErrNoSubject = errors.New("email: no subject specified") // ErrNoBody is returned when no body is specified ErrNoBody = errors.New("email: no body specified") // ErrInvalidHeader is returned when a header contains invalid characters ErrInvalidHeader = errors.New("email: invalid header") )
var ( // ErrPoolClosed is returned when an operation is attempted on a closed pool. ErrPoolClosed = errors.New("smtp: connection pool is closed") // ErrPoolTimeout is returned when a connection cannot be obtained within the wait timeout. ErrPoolTimeout = errors.New("smtp: connection pool wait timeout") )
Pool-related sentinel errors.
var ErrPanicked = errors.New("email: panic recovered in send pipeline")
ErrPanicked is returned when a panic is recovered by the recovery middleware.
Functions ¶
func BuildRawMessage ¶
BuildRawMessage builds a raw RFC 2822 MIME message from an Email. Useful for providers that accept raw messages (e.g., AWS SES). For DKIM-signed output, use BuildRawMessageWithDKIM.
func BuildRawMessageWithDKIM ¶
func BuildRawMessageWithDKIM(e *Email, dkim *DKIMConfig) ([]byte, error)
BuildRawMessageWithDKIM builds a raw RFC 2822 MIME message and signs it with a DKIM-Signature header using the provided config. A nil dkim returns the message unsigned (equivalent to BuildRawMessage).
func LoadTemplatesFromDir ¶
LoadTemplatesFromDir loads all templates from a directory on the local filesystem that match the given glob patterns (e.g. "*.html", "*.txt", "*.subject"). Multiple patterns can be provided and their results are merged.
Template names are derived from the filename without extension. Files that share the same base name but differ in extension are merged into a single Template: .html files become the HTML body, .txt files become the plain-text body, and .subject files become the subject line. For example, given "welcome.html", "welcome.txt", and "welcome.subject", one Template named "welcome" is returned with all three fields populated.
func LoadTemplatesFromFS ¶
LoadTemplatesFromFS loads all templates from an fs.FS that match the given glob patterns (e.g. "*.html", "*.txt", "*.subject"). Multiple patterns can be provided and their results are merged.
This works with embed.FS, os.DirFS, or any fs.FS implementation.
Template names are derived from the filename without extension. Files that share the same base name but differ in extension are merged into a single Template: .html files become the HTML body, .txt files become the plain-text body, and .subject files become the subject line. For example, given "welcome.html", "welcome.txt", and "welcome.subject", one Template named "welcome" is returned with all three fields populated.
.subject files are read literally — leading/trailing whitespace is trimmed so a trailing newline at end-of-file is not preserved in the subject.
func ParseDKIMPrivateKey ¶
ParseDKIMPrivateKey parses a PEM-encoded private key (RSA or Ed25519) for DKIM signing.
func SanitizeFuncMap ¶
func SanitizeFuncMap() htmltemplate.FuncMap
SanitizeFuncMap returns a htmltemplate.FuncMap with a "sanitize" function that sanitizes HTML using the default EmailPolicy.
Usage:
tmpl := template.New("email").Funcs(email.SanitizeFuncMap())
In templates:
{{.UserContent | sanitize}}
func SanitizeFuncMapWithPolicy ¶
func SanitizeFuncMapWithPolicy(p *Policy) htmltemplate.FuncMap
SanitizeFuncMapWithPolicy returns a htmltemplate.FuncMap with a "sanitize" function that uses a custom Policy.
func SanitizeHTML ¶
SanitizeHTML sanitizes HTML using the default EmailPolicy. It removes elements, attributes, and URL protocols that are unsafe or unsupported in email clients.
func SanitizeHTMLWithPolicy ¶
SanitizeHTMLWithPolicy sanitizes HTML using a custom Policy.
func SignMessage ¶
func SignMessage(msg []byte, config *DKIMConfig) ([]byte, error)
SignMessage signs a raw RFC 2822 message with a DKIM-Signature header. It returns a new message with the DKIM-Signature prepended.
Types ¶
type AsyncOption ¶
type AsyncOption func(*AsyncSender)
AsyncOption configures an AsyncSender.
func WithAsyncLogger ¶
func WithAsyncLogger(l Logger) AsyncOption
WithAsyncLogger sets the logger used for background error reporting.
func WithErrorHandler ¶
func WithErrorHandler(fn func(ctx context.Context, email *Email, err error)) AsyncOption
WithErrorHandler sets a callback that is invoked when a fire-and-forget send fails in the background.
func WithQueueSize ¶
func WithQueueSize(size int) AsyncOption
WithQueueSize sets the channel buffer size for the async queue. Default: 100.
func WithWorkers ¶
func WithWorkers(n int) AsyncOption
WithWorkers sets the number of worker goroutines that drain the queue. Default: 1.
type AsyncSender ¶
type AsyncSender struct {
// contains filtered or unexported fields
}
AsyncSender wraps a Sender and sends emails asynchronously via a buffered queue processed by background worker goroutines. It implements the Sender interface so it can be used anywhere a Sender is expected.
func NewAsyncSender ¶
func NewAsyncSender(sender Sender, opts ...AsyncOption) *AsyncSender
NewAsyncSender creates a new AsyncSender that wraps the given Sender. Workers are started immediately. Call Close to drain the queue and shut down.
func (*AsyncSender) Close ¶
func (a *AsyncSender) Close() error
Close stops accepting new emails, waits for in-flight work to complete, and closes the underlying sender. It is safe to call multiple times.
func (*AsyncSender) Send ¶
func (a *AsyncSender) Send(ctx context.Context, e *Email) error
Send validates the email eagerly and enqueues it for asynchronous delivery. It returns ErrQueueFull if the buffer is full, or ErrQueueClosed if the sender has been closed. Send implements the Sender interface.
The caller's context is detached with context.WithoutCancel before the task is queued: cancelling ctx after Send returns has no effect on the background delivery, but request-scoped values (trace IDs, etc.) are preserved. Use SendWait if you need to block on completion or propagate cancellation.
type Attachment ¶
Attachment represents an email attachment
type Canonicalization ¶
type Canonicalization string
Canonicalization represents a DKIM canonicalization algorithm.
const ( // CanonicalizationSimple is the "simple" canonicalization algorithm (RFC 6376 §3.4.1/§3.4.3). CanonicalizationSimple Canonicalization = "simple" // CanonicalizationRelaxed is the "relaxed" canonicalization algorithm (RFC 6376 §3.4.2/§3.4.4). CanonicalizationRelaxed Canonicalization = "relaxed" )
type DKIMConfig ¶
type DKIMConfig struct {
// Domain is the signing domain (d= tag). Required.
Domain string
// Selector is the DNS selector (s= tag). Required.
Selector string
// PrivateKey is the signer. Must be *rsa.PrivateKey or ed25519.PrivateKey.
// Use ParseDKIMPrivateKey to load from PEM data.
PrivateKey crypto.Signer
// HeaderCanonicalization is the canonicalization algorithm for headers.
// Default: CanonicalizationRelaxed.
HeaderCanonicalization Canonicalization
// BodyCanonicalization is the canonicalization algorithm for the body.
// Default: CanonicalizationRelaxed.
BodyCanonicalization Canonicalization
// SignedHeaders lists the header field names to sign.
// If empty, a sensible default set is used. "From" is always included.
SignedHeaders []string
// Expiration is the signature validity duration (x= tag).
// Zero means no expiration.
Expiration time.Duration
}
DKIMConfig holds the configuration for DKIM signing.
func (*DKIMConfig) Validate ¶
func (c *DKIMConfig) Validate() error
Validate checks that the DKIM configuration is valid.
type Email ¶
type Email struct {
From string
To []string
Cc []string
Bcc []string
ReplyTo string
Subject string
Body string
HTMLBody string
Attachments []Attachment
// Headers holds custom MIME headers. Prefer AddHeader, which canonicalizes
// the key (textproto.CanonicalMIMEHeaderKey) and rejects CRLF injection.
// Direct writes skip canonicalization, so "X-Foo" and "x-foo" will produce
// duplicate output headers. Validate() scrubs values for CRLF at build time
// regardless of how the entry was added.
Headers map[string]string
// contains filtered or unexported fields
}
Email represents an email message
func NewEmail ¶
func NewEmail() *Email
NewEmail creates a new email
Example ¶
email, err := NewEmail().
SetFrom("sender@example.com").
AddTo("recipient@example.com").
SetSubject("Hello").
SetBody("World").
Build()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(email.Subject)
Output: Hello
func (*Email) AddAttachment ¶
AddAttachment adds an attachment. The data slice is copied, so the caller is free to mutate the backing array afterwards.
func (*Email) AddHeader ¶
AddHeader adds a custom header. The key is normalized to canonical MIME form (e.g., "x-foo" → "X-Foo") so callers cannot accidentally produce duplicate headers via inconsistent casing.
func (*Email) SetHTMLBody ¶
SetHTMLBody sets the HTML body
func (*Email) SetReplyTo ¶
SetReplyTo sets the reply-to address
func (*Email) SetSubject ¶
SetSubject sets the subject
type Error ¶
type Error struct {
Op string // operation that failed (e.g., "send", "validate")
From string // sender address
To []string // recipient addresses
Err error // underlying error
}
Error represents an email operation error with context
type EventType ¶
type EventType string
EventType represents the type of a webhook delivery notification event.
const ( // EventDelivered indicates the message was accepted by the recipient's mail server. EventDelivered EventType = "delivered" // EventBounced indicates a hard bounce (permanent delivery failure). EventBounced EventType = "bounced" // EventDeferred indicates a soft bounce (temporary delivery failure). EventDeferred EventType = "deferred" // EventOpened indicates the recipient opened the email. EventOpened EventType = "opened" // EventClicked indicates the recipient clicked a link in the email. EventClicked EventType = "clicked" // EventComplained indicates the recipient marked the email as spam. EventComplained EventType = "complained" // EventUnsubscribed indicates the recipient unsubscribed. EventUnsubscribed EventType = "unsubscribed" // EventDropped indicates the provider rejected the message before sending. EventDropped EventType = "dropped" )
type Logger ¶
type Logger interface {
// Debug logs a debug message with optional key-value pairs
Debug(msg string, keysAndValues ...any)
// Info logs an info message with optional key-value pairs
Info(msg string, keysAndValues ...any)
// Warn logs a warning message with optional key-value pairs
Warn(msg string, keysAndValues ...any)
// Error logs an error message with optional key-value pairs
Error(msg string, keysAndValues ...any)
// With returns a new logger with the given key-value pairs added to context
With(keysAndValues ...any) Logger
}
Logger is the interface for logging within the email package. Users can provide their own implementation or use the provided adapters for popular logging libraries (slog, zap, logrus).
By default, if no logger is provided, logging is disabled (NoOpLogger).
keysAndValues must be supplied as alternating key/value pairs. An odd-length slice is backend-dependent: slog, for example, renders the dangling key as "!BADKEY". Always pass pairs.
func NewSlogLogger ¶
NewSlogLogger creates a new Logger from slog.Logger
type Mailer ¶
type Mailer struct {
// contains filtered or unexported fields
}
Mailer provides a high-level email sending interface. It is safe for concurrent use.
func NewMailerWithOptions ¶
func NewMailerWithOptions(sender Sender, from string, opts ...MailerOption) *Mailer
NewMailerWithOptions creates a new Mailer with the given options applied.
mailer := email.NewMailerWithOptions(sender, "from@example.com",
email.WithMiddleware(
email.WithLogging(logger),
email.WithRecovery(),
),
)
func (*Mailer) RegisterTemplate ¶
RegisterTemplate registers an email template. It is safe for concurrent use.
func (*Mailer) SendBatch ¶
SendBatch sends multiple emails concurrently with a concurrency limit. The concurrencyLimit parameter controls how many emails are sent simultaneously. If concurrencyLimit is <= 0, a default of 10 is used.
All emails are validated before sending begins. If any email fails validation, the entire batch fails without sending any emails.
If any email fails to send, the first error is returned. All emails are attempted regardless of individual failures.
type MailerOption ¶
type MailerOption func(*Mailer)
MailerOption configures a Mailer.
func WithMiddleware ¶
func WithMiddleware(middlewares ...Middleware) MailerOption
WithMiddleware returns a MailerOption that wraps the Mailer's Sender with the given middlewares using Chain.
type MetricsCollector ¶
type MetricsCollector interface {
// IncSendAttempt increments the counter for send attempts.
IncSendAttempt()
// IncSendSuccess increments the counter for successful sends.
IncSendSuccess()
// IncSendFailure increments the counter for failed sends.
IncSendFailure()
// ObserveSendDuration records the duration of a send operation.
ObserveSendDuration(duration time.Duration)
}
MetricsCollector defines the interface for collecting email send metrics. Implementations must be safe for concurrent use.
This interface is intentionally minimal. Implement it to feed metrics into Prometheus, StatsD, OpenTelemetry, or any other system.
type Middleware ¶
Middleware wraps a Sender to add cross-cutting behavior. It follows the same pattern as net/http middleware.
func WithHooks ¶
func WithHooks(hooks SendHooks) Middleware
WithHooks returns a Middleware that invokes the provided callbacks at each stage of the send lifecycle. Nil callbacks are safely skipped.
func WithLogging ¶
func WithLogging(logger Logger) Middleware
WithLogging returns a Middleware that logs send attempts using the provided Logger. It logs the start of each send, success with duration, and failure with duration and error.
func WithMetrics ¶
func WithMetrics(collector MetricsCollector) Middleware
WithMetrics returns a Middleware that records send metrics using the provided MetricsCollector.
func WithRecovery ¶
func WithRecovery() Middleware
WithRecovery returns a Middleware that catches panics in downstream Send calls and converts them to errors wrapping ErrPanicked.
func WithSanitization ¶
func WithSanitization() Middleware
WithSanitization returns a Middleware that sanitizes the HTMLBody of every email using the default EmailPolicy before sending. This acts as a safety net so that even if template authors forget to sanitize user content, the email body is cleaned before delivery.
func WithSanitizationPolicy ¶
func WithSanitizationPolicy(p *Policy) Middleware
WithSanitizationPolicy returns a Middleware that sanitizes the HTMLBody using a custom Policy.
type MockSender ¶
type MockSender struct {
// contains filtered or unexported fields
}
MockSender is a mock email sender for testing. It records emails in memory when sendFn succeeds (or when sendFn is nil) and provides methods to inspect them for test assertions.
func NewMockSender ¶
func NewMockSender() *MockSender
NewMockSender creates a new mock sender
Example ¶
mock := NewMockSender()
mailer := NewMailer(mock, "test@example.com")
ctx := context.Background()
if err := mailer.Send(ctx, []string{"user@example.com"}, "Test", "Hello"); err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(mock.GetEmailCount())
fmt.Println(mock.GetLastEmail().Subject)
Output: 1 Test
func (*MockSender) GetEmailCount ¶
func (m *MockSender) GetEmailCount() int
GetEmailCount returns the number of sent emails
func (*MockSender) GetEmailsBySubject ¶
func (m *MockSender) GetEmailsBySubject(subject string) []*Email
GetEmailsBySubject returns all emails with a specific subject
func (*MockSender) GetEmailsTo ¶
func (m *MockSender) GetEmailsTo(recipient string) []*Email
GetEmailsTo returns all emails sent to a specific recipient
func (*MockSender) GetLastEmail ¶
func (m *MockSender) GetLastEmail() *Email
GetLastEmail returns the last sent email, or nil if no emails have been sent
func (*MockSender) GetSentEmails ¶
func (m *MockSender) GetSentEmails() []*Email
GetSentEmails returns all sent emails
func (*MockSender) Send ¶
func (m *MockSender) Send(ctx context.Context, email *Email) error
Send records the email
func (*MockSender) SetSendFunc ¶
func (m *MockSender) SetSendFunc(fn func(ctx context.Context, email *Email) error)
SetSendFunc sets a custom send function for testing error scenarios. If nil, Send will succeed by default.
Example:
mock := email.NewMockSender()
mock.SetSendFunc(func(ctx context.Context, email *Email) error {
return errors.New("smtp connection failed")
})
type NoOpLogger ¶
type NoOpLogger struct{}
NoOpLogger is a logger that discards all logs. This is the default logger when none is provided.
type NoOpMetricsCollector ¶
type NoOpMetricsCollector struct{}
NoOpMetricsCollector is a metrics collector that discards all metrics. Useful as a default or in tests.
func (NoOpMetricsCollector) IncSendAttempt ¶
func (NoOpMetricsCollector) IncSendAttempt()
func (NoOpMetricsCollector) IncSendFailure ¶
func (NoOpMetricsCollector) IncSendFailure()
func (NoOpMetricsCollector) IncSendSuccess ¶
func (NoOpMetricsCollector) IncSendSuccess()
func (NoOpMetricsCollector) ObserveSendDuration ¶
func (NoOpMetricsCollector) ObserveSendDuration(_ time.Duration)
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy defines which HTML elements and attributes are allowed in sanitized output. Use EmailPolicy for a sensible default or build a custom policy with NewPolicy.
func EmailPolicy ¶
func EmailPolicy() *Policy
EmailPolicy returns a Policy configured with elements and attributes that are safe and widely supported across email clients (Gmail, Outlook, Apple Mail, Yahoo Mail).
func NewPolicy ¶
func NewPolicy() *Policy
NewPolicy returns an empty policy that strips all HTML. Use the builder methods to allow specific elements and attributes.
func (*Policy) AllowAttributes ¶
AllowAttributes adds allowed attributes for the given element. The element is implicitly added to the allowlist if not already present.
func (*Policy) AllowElements ¶
AllowElements adds elements to the allowlist with no extra attributes beyond globals. If an element was already allowed, its attribute set is preserved.
func (*Policy) AllowGlobalAttributes ¶
AllowGlobalAttributes adds attributes that are allowed on every element.
func (*Policy) AllowURLProtocols ¶
AllowURLProtocols sets the allowed URL protocols for an attribute (typically "href" or "src"). Values should be lowercase without a trailing colon (e.g. "https", "mailto").
func (*Policy) StripElements ¶
StripElements marks elements for complete removal — both the tags and their inner content are discarded. This is appropriate for elements like <script> and <style> where the content itself is dangerous.
type SMTPConfig ¶
type SMTPConfig struct {
// Host is the SMTP server hostname
Host string
// Port is the SMTP server port (typically 587 for TLS, 465 for SSL)
Port int
// Username is the SMTP authentication username
Username string
// Password is the SMTP authentication password
Password string
// From is the default sender email address
From string
// UseTLS enables STARTTLS encryption
UseTLS bool
// Timeout is the connection timeout (default: 30s)
Timeout time.Duration
// MaxRetries is the maximum number of retry attempts (default: 3).
// Set to a negative value to disable retries.
MaxRetries int
// RetryDelay is the initial retry delay (default: 1s)
RetryDelay time.Duration
// RetryBackoff is the exponential backoff multiplier (default: 2.0)
RetryBackoff float64
// RateLimit is the maximum number of emails per second (default: 10).
// Set to a negative value to disable rate limiting.
RateLimit int
// PoolSize is the maximum number of open SMTP connections in the pool.
// 0 (default) disables pooling — each Send dials a fresh connection.
PoolSize int
// MaxIdleConns is the maximum number of idle connections kept in the pool.
// Default: 2. Only used when PoolSize > 0.
MaxIdleConns int
// PoolMaxLifetime is the maximum lifetime of a pooled connection.
// Connections older than this are discarded on checkout. Default: 30m.
PoolMaxLifetime time.Duration
// PoolMaxIdleTime is the maximum idle time before a connection is evicted.
// Default: 5m.
PoolMaxIdleTime time.Duration
// MaxMessages is the maximum number of messages sent on a single connection
// before it is rotated. Default: 100.
MaxMessages int
// PoolWaitTimeout is the maximum time to wait for a connection when the
// pool is exhausted. Default: 5s.
PoolWaitTimeout time.Duration
// Logger is the logger interface for observability
// If nil, logging is disabled (NoOpLogger used)
Logger Logger
// DKIM is the optional DKIM signing configuration.
// If non-nil, all outgoing messages are signed with a DKIM-Signature header.
DKIM *DKIMConfig
}
SMTPConfig holds SMTP configuration
func (SMTPConfig) Validate ¶
func (c SMTPConfig) Validate() error
Validate validates the SMTP configuration
type SMTPSender ¶
type SMTPSender struct {
// contains filtered or unexported fields
}
SMTPSender sends emails via SMTP
func NewSMTPSender ¶
func NewSMTPSender(config SMTPConfig) (*SMTPSender, error)
NewSMTPSender creates a new SMTP sender. It validates the config and returns an error if it is invalid.
func (*SMTPSender) Close ¶
func (s *SMTPSender) Close() error
Close closes the SMTP sender. If connection pooling is enabled, it shuts down the pool and closes all idle connections.
type SendHooks ¶
type SendHooks struct {
// OnSend is called before each send attempt.
OnSend func(ctx context.Context, e *Email)
// OnSuccess is called after a successful send, with the duration.
OnSuccess func(ctx context.Context, e *Email, duration time.Duration)
// OnFailure is called after a failed send, with the duration and error.
OnFailure func(ctx context.Context, e *Email, duration time.Duration, err error)
}
SendHooks defines optional callbacks for email send lifecycle events. All fields are optional; nil callbacks are skipped. Callbacks must be safe for concurrent use.
type Sender ¶
type Sender interface {
// Send sends an email
Send(ctx context.Context, email *Email) error
// Close closes the sender connection
Close() error
}
Sender defines the interface for email senders
func Chain ¶
func Chain(sender Sender, middlewares ...Middleware) Sender
Chain applies middlewares to a Sender in order. The first middleware in the list is the outermost (executed first).
wrapped := email.Chain(sender, loggingMW, recoveryMW, metricsMW)
A call to wrapped.Send() executes: logging -> recovery -> metrics -> sender.
type SlogLogger ¶
type SlogLogger struct {
// contains filtered or unexported fields
}
SlogLogger wraps slog.Logger to implement the email.Logger interface. This adapter allows using Go's standard library structured logger (slog) with the email package.
Example:
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
config := email.SMTPConfig{
Host: "smtp.gmail.com",
Logger: email.NewSlogLogger(logger),
}
func (*SlogLogger) Debug ¶
func (l *SlogLogger) Debug(msg string, keysAndValues ...any)
Debug implements Logger
func (*SlogLogger) Error ¶
func (l *SlogLogger) Error(msg string, keysAndValues ...any)
Error implements Logger
func (*SlogLogger) Info ¶
func (l *SlogLogger) Info(msg string, keysAndValues ...any)
Info implements Logger
func (*SlogLogger) Warn ¶
func (l *SlogLogger) Warn(msg string, keysAndValues ...any)
Warn implements Logger
func (*SlogLogger) With ¶
func (l *SlogLogger) With(keysAndValues ...any) Logger
With implements Logger
type Template ¶
type Template struct {
// contains filtered or unexported fields
}
Template represents an email template
func LoadTemplateFromFS ¶
LoadTemplateFromFS loads a single template from an fs.FS. This works with embed.FS, os.DirFS, or any other fs.FS implementation. Files ending in .txt are loaded as the plain-text body; all others are loaded as the HTML body.
func LoadTemplateFromFile ¶
LoadTemplateFromFile loads a template from a file. The file content is used as the HTML template.
func NewTemplate ¶
NewTemplate creates a new email template
Example ¶
tmpl := NewTemplate("welcome")
tmpl.SetSubject("Welcome {{.Name}}")
if _, err := tmpl.SetTextTemplate("Hello {{.Name}}, welcome!"); err != nil {
fmt.Println("error:", err)
return
}
email, err := tmpl.Render(map[string]any{"Name": "Alice"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(email.Subject)
fmt.Println(email.Body)
Output: Welcome Alice Hello Alice, welcome!
func (*Template) SetHTMLTemplate ¶
SetHTMLTemplate sets the HTML template
func (*Template) SetSubject ¶
SetSubject sets the subject template
func (*Template) SetTextTemplate ¶
SetTextTemplate sets the plain text template
func (*Template) WithSanitization ¶
WithSanitization enables HTML sanitization on rendered output using the default EmailPolicy. The HTMLBody is sanitized after template rendering.
type WebhookEvent ¶
type WebhookEvent struct {
// Type is the normalized event type.
Type EventType
// MessageID is the provider-assigned message identifier.
MessageID string
// Recipient is the email address this event relates to.
Recipient string
// Timestamp is when the event occurred at the provider.
Timestamp time.Time
// Provider identifies the source (e.g. "sendgrid", "mailgun", "ses").
Provider string
// Reason contains detail for bounces, drops, or deferrals.
// Empty for positive events like delivered/opened.
Reason string
// URL is the clicked URL for EventClicked events. Empty otherwise.
URL string
// UserAgent is the user agent for open/click events when available.
UserAgent string
// IP is the IP address associated with the event when available.
IP string
// Tags contains provider-specific tags/categories for the message.
Tags []string
// Metadata holds arbitrary provider-specific key-value data that doesn't
// map to a named field.
Metadata map[string]string
// RawPayload is the original bytes from the provider, preserved for
// debugging or provider-specific processing.
RawPayload []byte
}
WebhookEvent is the provider-agnostic representation of a delivery notification event. Provider-specific parsers (in providers/ submodules) normalize raw webhook payloads into this type.
type WebhookHandler ¶
type WebhookHandler interface {
// HandleEvent processes a single webhook event.
// Returning an error causes the WebhookReceiver to respond with 500,
// signaling the provider to retry delivery.
HandleEvent(ctx context.Context, event WebhookEvent) error
}
WebhookHandler handles normalized webhook events. Implementations must be safe for concurrent use.
type WebhookHandlerFunc ¶
type WebhookHandlerFunc func(ctx context.Context, event WebhookEvent) error
WebhookHandlerFunc is an adapter to allow ordinary functions to be used as WebhookHandlers, following the net/http.HandlerFunc pattern.
func (WebhookHandlerFunc) HandleEvent ¶
func (f WebhookHandlerFunc) HandleEvent(ctx context.Context, event WebhookEvent) error
HandleEvent calls f(ctx, event).
type WebhookParser ¶
type WebhookParser interface {
// Parse reads the HTTP request and returns zero or more normalized events.
// Providers like SendGrid batch multiple events per request.
Parse(r *http.Request) ([]WebhookEvent, error)
}
WebhookParser parses provider-specific HTTP requests into WebhookEvents. Each provider adapter (providers/webhooksendgrid, etc.) implements this interface. Implementations must be safe for concurrent use.
type WebhookReceiver ¶
type WebhookReceiver struct {
// contains filtered or unexported fields
}
WebhookReceiver is an http.Handler that receives webhook POSTs from email providers, parses them using a WebhookParser, and dispatches normalized events to a WebhookHandler.
It is safe for concurrent use.
receiver := email.NewWebhookReceiver(parser, handler)
http.Handle("/webhooks/email", receiver)
func NewWebhookReceiver ¶
func NewWebhookReceiver(parser WebhookParser, handler WebhookHandler, opts ...WebhookReceiverOption) *WebhookReceiver
NewWebhookReceiver creates a new WebhookReceiver.
func (*WebhookReceiver) ServeHTTP ¶
func (wr *WebhookReceiver) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler. It parses the request, dispatches each event to the handler, and responds:
- 200 OK if all events are handled successfully
- 400 Bad Request if the request cannot be parsed
- 405 Method Not Allowed if the method is not POST
- 500 Internal Server Error if any handler returns an error
All events in the batch are dispatched even if one fails — the receiver does not short-circuit on the first error. This avoids dropping later events in a batch when a transient error occurs in the middle, but it means handlers MUST be idempotent: a 500 response causes the provider to redeliver the entire batch, including events that already succeeded.
type WebhookReceiverOption ¶
type WebhookReceiverOption func(*WebhookReceiver)
WebhookReceiverOption configures a WebhookReceiver.
func WithEventFilter ¶
func WithEventFilter(types ...EventType) WebhookReceiverOption
WithEventFilter restricts the receiver to only dispatch the listed event types. Events not in the filter are silently acknowledged (200 OK) but not dispatched to the handler.
func WithWebhookLogger ¶
func WithWebhookLogger(l Logger) WebhookReceiverOption
WithWebhookLogger sets the logger for the webhook receiver.