mailer is a high-performance, provider-agnostic Go library for sending emails with a focus on transactional email use cases. It provides a clean, idiomatic Go API with support for multiple email providers, template management, and built-in reliability features.
- 🚀 Provider Agnostic: Support for AWS SES, SendGrid, Mailgun, and SMTP, plus a dry-run provider for local development
- 📧 Template Management: HTML/text templates with helper functions
- 🔄 Automatic Retries: Exponential backoff with jitter
- 🚦 Rate Limiting: Configurable rate limiting per provider or recipient
- 🔌 Circuit Breaker: Fault tolerance with circuit breaker pattern
- 📊 Observability: Built-in distributed tracing with OpenTelemetry
- 🏗️ Batch Operations: Efficient batch email sending
- ⚡ Context Support: Full context.Context propagation
- 🔒 Thread Safe: All operations are safe for concurrent use
- 🎯 Type Safe: Leverages Go's type system for compile-time safety
go get github.com/lattiq/mailerpackage main
import (
"context"
"log"
"github.com/lattiq/mailer"
)
func main() {
// Create client with SMTP provider
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithSMTP("localhost", "1025"),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create and send email
email := &mailer.Email{
From: mailer.Address{Email: "sender@example.com", Name: "Sender"},
To: []mailer.Address{{Email: "recipient@example.com", Name: "Recipient"}},
Subject: "Welcome!",
HTMLBody: "<h1>Welcome to our service!</h1>",
TextBody: "Welcome to our service!",
}
err = client.Send(context.Background(), email)
if err != nil {
log.Fatal(err)
}
log.Println("Email sent successfully!")
}client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithAWSSES("us-east-1"),
)With explicit credentials:
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithAWSSESCredentials("us-east-1", "access-key", "secret-key"),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithSendGrid("your-api-key"),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithMailgun("your-api-key", "your-domain.com"),
)For Mailgun EU:
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithMailgunEU("your-api-key", "your-domain.com"),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithSMTPAuth("smtp.gmail.com", "587", "username", "password"),
)With TLS:
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithSMTPTLS("smtp.gmail.com", "465", "username", "password", false),
)A no-delivery provider that records every send to slog instead of dispatching it. Intended for local development and tests — do not use in production.
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithDryRun(mailer.DryRunOptions{}),
)By default each entry logs the envelope (from, to, subject) and body sizes. Set IncludeText or IncludeHTML to also dump the rendered bodies — useful when debugging template rendering, but produces large log lines:
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithDryRun(mailer.DryRunOptions{
IncludeText: true,
IncludeHTML: true,
}),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithRetry(3, 100*time.Millisecond, 5*time.Second, 2.0),
mailer.WithJitter(true),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithRateLimit(100, time.Minute, 10), // 100 emails per minute, burst of 10
mailer.WithPerRecipientRateLimit(true),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithCircuitBreaker(5, 3, 60*time.Second), // 5 failures trigger, 3 successes to close, 60s timeout
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithAWSSES("us-east-1"),
mailer.WithFallbackProvider(mailer.ProviderSendGrid, mailer.ProviderSettings{
"api_key": "your-sendgrid-key",
}),
)config := mailer.DefaultConfig()
config.Templates.Enabled = true
config.Templates.Directory = "templates"
config.Templates.Extension = []string{".html", ".text"}
client, err := mailer.New(config, mailer.WithAWSSES("us-east-1"))
if err != nil {
log.Fatal(err)
}
defer client.Close()Templates use double extensions to specify both the template name and format:
templates/
├── welcome.html.html # HTML template for welcome emails
├── welcome.text.text # Text template for welcome emails
├── otp.html.html # HTML template for OTP emails
└── otp.text.text # Text template for OTP emails
Important: Template files must use double extensions (e.g., name.html.html, name.text.text) so that when the file extension is removed during loading, the templates are registered with names like name.html and name.text, which is what SendTemplate expects.
templateRequest := &mailer.TemplateRequest{
Template: "otp", // Template name (without extension)
From: mailer.Address{Email: "noreply@example.com", Name: "Example"},
To: []mailer.Address{{Email: "user@example.com", Name: "User"}},
Subject: "Your OTP Code",
Data: map[string]interface{}{
"UserName": "John Doe",
"OTP": "123456",
"ExpiryTime": "10 minutes",
},
Headers: map[string]string{
"X-Category": "authentication",
},
}
err := client.SendTemplate(context.Background(), templateRequest)emails := []*mailer.Email{
{
From: mailer.Address{Email: "noreply@example.com"},
To: []mailer.Address{{Email: "user1@example.com"}},
Subject: "Batch Email 1",
TextBody: "First email in batch",
},
{
From: mailer.Address{Email: "noreply@example.com"},
To: []mailer.Address{{Email: "user2@example.com"}},
Subject: "Batch Email 2",
TextBody: "Second email in batch",
},
}
err := client.SendBatch(context.Background(), emails)Common tasks are wrapped in the Makefile:
make install # install dev tools (golangci-lint, goimports)
make build # verify the package compiles
make test # run tests
make lint # run golangci-lint
make check # lint + test (CI gate)
make fix # format, lint --fix, and go mod tidy
make patch # bump patch version and create a git tagRun make help to see the full target list.
client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithTracing("my-service", "1.0.0", 1.0),
mailer.WithTracingEndpoint("http://jaeger:14268/api/traces"),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithMetrics("myapp_mailer", 30*time.Second),
)client, err := mailer.New(
mailer.DefaultConfig(),
mailer.WithLogging("info", "json", "stdout"),
mailer.WithRequestResponseLogging(false), // Be careful with sensitive data
)The library provides rich error types for different scenarios:
err := client.Send(ctx, email)
if err != nil {
switch {
case mailer.IsRetryable(err):
log.Println("Error is retryable:", err)
case mailer.IsTemporary(err):
log.Println("Error is temporary:", err)
default:
log.Println("Permanent error:", err)
}
// Check for specific error types
var validationErr *mailer.ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error in field %s: %s", validationErr.Field, validationErr.Message)
}
var providerErr *mailer.ProviderError
if errors.As(err, &providerErr) {
log.Printf("Provider %s error [%s]: %s", providerErr.Provider, providerErr.Code, providerErr.Message)
}
}email := &mailer.Email{
From: mailer.Address{Email: "sender@example.com"},
To: []mailer.Address{{Email: "recipient@example.com"}},
Subject: "Test",
TextBody: "Hello, World!",
}
if err := email.Validate(); err != nil {
log.Printf("Email validation failed: %v", err)
}- Connection Pooling: HTTP connections are pooled automatically
- Memory Usage: ~1KB per email (excluding attachments)
- Throughput: <10ms processing time per email (excluding network)
- Template Caching: Templates are cached for improved performance
- Batch Operations: ~1ms per email in batch operations
- Input Validation: All email addresses and headers are validated
- Header Injection: Protection against email header injection attacks
- TLS Support: Full TLS/SSL support for SMTP connections
- Credential Management: Support for environment variables and IAM roles
This project is licensed under the MIT License - see the LICENSE file for details.
For more examples, see the examples/ directory:
- OTP Email with Templates - Complete example showing how to send OTP emails using templates with AWS SES