email

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 37 Imported by: 0

README

📧 goemail

Go Reference Go Report Card Go Version CI GitHub tag codecov

Production-ready email package for Go with SMTP support, templating, and retry logic.

✨ Features

  • 📤 SMTP Support - TLS/STARTTLS, authentication
  • 📝 Templating - Go templates for HTML and plain text emails
  • 📎 Attachments - Send files with proper MIME encoding
  • 🔄 Retry Logic - Exponential backoff with configurable attempts
  • Rate Limiting - Built-in rate limiting to prevent overwhelming servers
  • 🔍 Logging Interface - Bring your own logger (slog, zap, logrus, etc.)
  • 🧪 Testing - Mock sender for easy testing
  • 🎯 Builder API - Fluent, chainable API for constructing emails
  • 🔒 Security - Email header injection protection, address validation
  • 🎨 Batch Sending - Send multiple emails concurrently with limits
  • 🔗 Connection Pooling - Reuse SMTP connections for high-throughput sending
  • 🔌 Middleware Pipeline - Composable middleware for logging, metrics, recovery, and hooks
  • 🔀 Async Sending - Background queue worker with configurable workers and buffer
  • 🌐 Provider Adapters - SendGrid, Mailgun, and AWS SES via HTTP APIs (providers modules)
  • ✍️ DKIM Signing - Sign outgoing emails with RSA-SHA256 or Ed25519-SHA256 (RFC 6376/8463)
  • 📊 Context Support - Full context.Context integration for timeouts and cancellation

📦 Installation

go get github.com/KARTIKrocks/goemail

🚀 Quick Start

Basic Email
package main

import (
    "context"
    "log"
    "time"

    "github.com/KARTIKrocks/goemail"
)

func main() {
    // Configure SMTP
    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,
    }

    // Create sender and mailer
    sender, err := email.NewSMTPSender(config)
    if err != nil {
        log.Fatal(err)
    }
    mailer := email.NewMailer(sender, config.From)
    defer mailer.Close()

    // Send email
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    err = mailer.Send(ctx,
        []string{"recipient@example.com"},
        "Hello!",
        "This is a test email.",
    )

    if err != nil {
        log.Fatal(err)
    }
}
HTML Email with Template
// Create template
tmpl := email.NewTemplate("welcome")
tmpl.SetSubject("Welcome {{.Name}}!")

tmpl.SetHTMLTemplate(`
<!DOCTYPE html>
<html>
<body>
    <h1>Hello {{.Name}}!</h1>
    <p>Welcome to our service. Click below to get started:</p>
    <a href="{{.VerifyLink}}">Verify Email</a>
</body>
</html>
`)

// Register template
mailer.RegisterTemplate("welcome", tmpl)

// Send using template
data := map[string]any{
    "Name":       "John Doe",
    "VerifyLink": "https://example.com/verify/abc123",
}

err := mailer.SendTemplate(ctx, []string{"john@example.com"}, "welcome", data)
With Attachments
// Read file
pdfData, _ := os.ReadFile("invoice.pdf")

email := email.NewEmail().
    SetFrom("billing@example.com").
    AddTo("customer@example.com").
    SetSubject("Your Invoice").
    SetBody("Please find your invoice attached.").
    AddAttachment("invoice.pdf", "application/pdf", pdfData)

err := mailer.SendEmail(ctx, email)

📖 Documentation

Configuration
type SMTPConfig struct {
    Host            string        // SMTP server hostname
    Port            int           // SMTP server port (typically 587 for TLS, 465 for SSL)
    Username        string        // SMTP username
    Password        string        // SMTP password
    From            string        // Default sender email
    UseTLS          bool          // Use STARTTLS
    Timeout         time.Duration // Connection timeout (default: 30s)
    MaxRetries      int           // Max retry attempts (default: 3)
    RetryDelay      time.Duration // Initial retry delay (default: 1s)
    RetryBackoff    float64       // Backoff multiplier (default: 2.0)
    RateLimit       int           // Max emails per second (default: 10)
    PoolSize        int           // Max pooled connections (0 = disabled)
    MaxIdleConns    int           // Max idle connections (default: 2)
    PoolMaxLifetime time.Duration // Max connection lifetime (default: 30m)
    PoolMaxIdleTime time.Duration // Max idle time before eviction (default: 5m)
    MaxMessages     int           // Max messages per connection (default: 100)
    PoolWaitTimeout time.Duration // Max wait when pool full (default: 5s)
    Logger          Logger        // Optional logger interface
    DKIM            *DKIMConfig   // Optional DKIM signing configuration
}
Common SMTP Providers
Gmail
config := email.SMTPConfig{
    Host:     "smtp.gmail.com",
    Port:     587,
    Username: "your-email@gmail.com",
    Password: "your-app-password", // Use App Password, not regular password
    UseTLS:   true,
}

Note: Enable 2FA and create an App Password at https://myaccount.google.com/apppasswords

SendGrid
config := email.SMTPConfig{
    Host:     "smtp.sendgrid.net",
    Port:     587,
    Username: "apikey",
    Password: "your-sendgrid-api-key",
    UseTLS:   true,
}
AWS SES
config := email.SMTPConfig{
    Host:     "email-smtp.us-east-1.amazonaws.com",
    Port:     587,
    Username: "your-ses-smtp-username",
    Password: "your-ses-smtp-password",
    UseTLS:   true,
}
Mailgun
config := email.SMTPConfig{
    Host:     "smtp.mailgun.org",
    Port:     587,
    Username: "postmaster@your-domain.mailgun.org",
    Password: "your-mailgun-smtp-password",
    UseTLS:   true,
}
Email Builder API
msg := email.NewEmail().
    SetFrom("sender@example.com").
    AddTo("user1@example.com", "user2@example.com").
    AddCc("manager@example.com").
    AddBcc("archive@example.com").
    SetReplyTo("support@example.com").
    SetSubject("Important Update").
    SetBody("Plain text body").
    SetHTMLBody("<h1>HTML body</h1>").
    AddHeader("X-Priority", "1").
    AddAttachment("report.pdf", "application/pdf", pdfData)

// Validate
built, err := msg.Build()
if err != nil {
    log.Fatal(err)
}

if err := sender.Send(ctx, built); err != nil {
    log.Fatal(err)
}
Batch Sending
emails := []*email.Email{
    email.NewEmail().SetFrom("no-reply@example.com").AddTo("user1@example.com").SetSubject("Hi").SetBody("Hello"),
    email.NewEmail().SetFrom("no-reply@example.com").AddTo("user2@example.com").SetSubject("Hi").SetBody("Hello"),
    email.NewEmail().SetFrom("no-reply@example.com").AddTo("user3@example.com").SetSubject("Hi").SetBody("Hello"),
}

// Send with concurrency limit of 5
err := mailer.SendBatch(ctx, emails, 5)
Connection Pooling

For high-throughput sending, enable SMTP connection pooling to reuse established connections and avoid 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 max 5 connections
}

sender, err := email.NewSMTPSender(config)
if err != nil {
    log.Fatal(err)
}
defer sender.Close() // Important: closes all pooled connections

mailer := email.NewMailer(sender, config.From)

// Send many emails — connections are reused automatically
for _, recipient := range recipients {
    err := mailer.Send(ctx, []string{recipient}, "Hello!", "Message body")
    if err != nil {
        log.Printf("send to %s failed: %v", recipient, err)
    }
}

Pool configuration options:

Field Default Description
PoolSize 0 (disabled) Max open connections
MaxIdleConns 2 Max idle connections in pool
PoolMaxLifetime 30m Max connection lifetime
PoolMaxIdleTime 5m Max idle time before eviction
MaxMessages 100 Max messages per connection before rotation
PoolWaitTimeout 5s Max wait when pool is exhausted
Middleware Pipeline

Add cross-cutting concerns like logging, metrics, and panic recovery using composable middleware:

// Create your sender (SMTP, mock, etc.)
sender, _ := email.NewSMTPSender(config)

// Wrap with middleware using Chain
wrapped := email.Chain(sender,
    email.WithRecovery(),              // Catch panics
    email.WithLogging(logger),         // Log sends
    email.WithHooks(email.SendHooks{   // Lifecycle callbacks
        OnSuccess: func(ctx context.Context, e *email.Email, d time.Duration) {
            fmt.Printf("sent to %v in %s\n", e.To, d)
        },
    }),
    email.WithMetrics(myCollector),    // Record metrics
)

mailer := email.NewMailer(wrapped, config.From)

Or use NewMailerWithOptions for a more compact setup:

mailer := email.NewMailerWithOptions(sender, config.From,
    email.WithMiddleware(
        email.WithRecovery(),
        email.WithLogging(logger),
        email.WithMetrics(myCollector),
    ),
)

Built-in middlewares:

Middleware Description
WithLogging(Logger) Logs send start, success/failure with duration
WithRecovery() Catches panics, returns ErrPanicked error
WithHooks(SendHooks) OnSend, OnSuccess, OnFailure callbacks
WithMetrics(MetricsCollector) Counters + duration via pluggable interface

Custom middleware: Implement the Middleware type (func(Sender) Sender) to add your own behavior (tracing, throttling, etc.).

OpenTelemetry Tracing

Add distributed tracing to email sends with the providers/otelmail submodule. It lives in a separate Go module so the core library stays dependency-free.

go get github.com/KARTIKrocks/goemail/providers/otelmail
import "github.com/KARTIKrocks/goemail/providers/otelmail"

// Wrap your sender with OTel tracing
wrapped := email.Chain(sender,
    otelmail.WithTracing(),          // creates a span per Send
    email.WithLogging(logger),
)

Each span includes attributes: email.from, email.to, email.subject, email.recipients.count. On failure the span records the error and sets status to Error.

Options:

  • otelmail.WithTracerProvider(tp) — use a custom TracerProvider (default: global)
  • otelmail.WithTracerName(name) — custom tracer name
  • otelmail.WithSpanName(name) — custom span name (default: "email.send")
Async Sending

For non-blocking email delivery, wrap any Sender with AsyncSender. Emails are validated eagerly and queued for background workers:

sender, _ := email.NewSMTPSender(config)

async := email.NewAsyncSender(sender,
    email.WithQueueSize(200),   // Buffer up to 200 emails
    email.WithWorkers(3),       // 3 background workers
    email.WithErrorHandler(func(ctx context.Context, e *email.Email, err error) {
        log.Printf("failed to send to %v: %v", e.To, err)
    }),
)
defer async.Close() // Drains queue, waits for workers, closes underlying sender

// Fire-and-forget — returns immediately
err := async.Send(ctx, myEmail)

// Or block until delivered
err = async.SendWait(ctx, myEmail)

AsyncSender implements the Sender interface, so it composes with middleware and Mailer:

wrapped := email.Chain(async, email.WithLogging(logger))
mailer := email.NewMailer(wrapped, "from@example.com")
Provider Adapters

Send emails via HTTP APIs instead of SMTP. Each adapter is a separate Go module under providers/ with no extra dependencies on the core library.

SendGrid
go get github.com/KARTIKrocks/goemail/providers/sendgrid
import "github.com/KARTIKrocks/goemail/providers/sendgrid"

sender, err := sendgrid.New(sendgrid.Config{
    APIKey: os.Getenv("SENDGRID_API_KEY"),
})
Mailgun
go get github.com/KARTIKrocks/goemail/providers/mailgun
import "github.com/KARTIKrocks/goemail/providers/mailgun"

sender, err := mailgun.New(mailgun.Config{
    Domain: "mg.example.com",
    APIKey: os.Getenv("MAILGUN_API_KEY"),
    // BaseURL: "https://api.eu.mailgun.net", // for EU accounts
})
AWS SES
go get github.com/KARTIKrocks/goemail/providers/ses
import "github.com/KARTIKrocks/goemail/providers/ses"

sender, err := ses.New(context.Background(), ses.Config{
    Region: "us-east-1",
})

All adapters implement email.Sender — use them with Mailer, middleware, AsyncSender, or directly.

DKIM Signing

Sign outgoing emails with DKIM-Signature headers for improved deliverability and authentication. Supports RSA-SHA256 and Ed25519-SHA256 algorithms with zero additional dependencies.

With SMTP
// Load your DKIM private key
pemData, _ := os.ReadFile("dkim-private.pem")
privateKey, err := email.ParseDKIMPrivateKey(pemData)
if err != nil {
    log.Fatal(err)
}

config := email.SMTPConfig{
    Host:     "smtp.example.com",
    Port:     587,
    Username: "user@example.com",
    Password: "password",
    UseTLS:   true,
    DKIM: &email.DKIMConfig{
        Domain:     "example.com",
        Selector:   "default",       // DNS record: default._domainkey.example.com
        PrivateKey: privateKey,
        // Optional:
        // HeaderCanonicalization: email.CanonicalizationRelaxed, // default
        // BodyCanonicalization:   email.CanonicalizationRelaxed, // default
        // Expiration:             24 * time.Hour,                // signature validity
    },
}

sender, _ := email.NewSMTPSender(config)
// All emails sent through this sender are automatically DKIM-signed
With Raw Messages (SES, custom providers)
e := email.NewEmail().
    SetFrom("sender@example.com").
    AddTo("recipient@example.com").
    SetSubject("Signed Email").
    SetBody("This email is DKIM-signed.")

dkimConfig := &email.DKIMConfig{
    Domain:     "example.com",
    Selector:   "default",
    PrivateKey: privateKey,
}

msg, err := email.BuildRawMessageWithDKIM(e, dkimConfig)
// msg now contains the DKIM-Signature header
Standalone Signing
// Sign any raw RFC 2822 message
signedMsg, err := email.SignMessage(rawMessage, dkimConfig)
Templates
Creating Templates
// HTML + Text template
tmpl := email.NewTemplate("newsletter")
tmpl.SetSubject("Newsletter - {{.Month}} {{.Year}}")

tmpl.SetTextTemplate("Hello {{.Name}}, check out our newsletter at {{.Link}}")

tmpl.SetHTMLTemplate(`
<!DOCTYPE html>
<html>
<body>
    <h2>{{.Title}}</h2>
    <p>Hello {{.Name}},</p>
    <div>{{.Content}}</div>
    <a href="{{.Link}}">Read More</a>
</body>
</html>
`)

// Register
mailer.RegisterTemplate("newsletter", tmpl)

// Send
data := map[string]any{
    "Name":    "Jane",
    "Month":   "January",
    "Year":    2024,
    "Title":   "Monthly Update",
    "Content": "Here's what's new...",
    "Link":    "https://example.com/newsletter",
}

err := mailer.SendTemplate(ctx, []string{"jane@example.com"}, "newsletter", data)
Loading from Files
// Load HTML template from file
tmpl, err := email.LoadTemplateFromFile("welcome", "templates/welcome.html")
if err != nil {
    log.Fatal(err)
}

mailer.RegisterTemplate("welcome", tmpl)
Logging

The package uses a simple Logger interface, allowing you to integrate with any logging library.

Using slog (Go 1.21+)
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

config := email.SMTPConfig{
    Host:   "smtp.gmail.com",
    Logger: email.NewSlogLogger(logger),
}
Custom Logger

Implement the Logger interface to integrate any logging library (zap, logrus, zerolog, etc.).

type MyLogger struct{}

func (l MyLogger) Debug(msg string, keysAndValues ...any) {
    // Your implementation
}

func (l MyLogger) Info(msg string, keysAndValues ...any) {
    // Your implementation
}

func (l MyLogger) Warn(msg string, keysAndValues ...any) {
    // Your implementation
}

func (l MyLogger) Error(msg string, keysAndValues ...any) {
    // Your implementation
}

func (l MyLogger) With(keysAndValues ...any) email.Logger {
    return l
}

config := email.SMTPConfig{
    Host:   "smtp.gmail.com",
    Logger: MyLogger{},
}
Testing
func TestSendWelcomeEmail(t *testing.T) {
    // Create mock sender
    mock := email.NewMockSender()
    mailer := email.NewMailer(mock, "test@example.com")

    // Send email
    ctx := context.Background()
    err := mailer.Send(ctx,
        []string{"user@example.com"},
        "Welcome",
        "Welcome to our service!",
    )

    if err != nil {
        t.Fatalf("send failed: %v", err)
    }

    // Verify
    if mock.GetEmailCount() != 1 {
        t.Errorf("expected 1 email, got %d", mock.GetEmailCount())
    }

    email := mock.GetLastEmail()
    if email.Subject != "Welcome" {
        t.Errorf("expected subject 'Welcome', got '%s'", email.Subject)
    }

    if email.To[0] != "user@example.com" {
        t.Errorf("expected to 'user@example.com', got '%s'", email.To[0])
    }
}

🔒 Security Best Practices

1. Use Environment Variables
config := email.SMTPConfig{
    Host:     os.Getenv("SMTP_HOST"),
    Port:     getEnvInt("SMTP_PORT", 587),
    Username: os.Getenv("SMTP_USERNAME"),
    Password: os.Getenv("SMTP_PASSWORD"),
    UseTLS:   true,
}

func getEnvInt(key string, defaultVal int) int {
    if val := os.Getenv(key); val != "" {
        if i, err := strconv.Atoi(val); err == nil {
            return i
        }
    }
    return defaultVal
}
2. Validate Emails

The package automatically validates email addresses, but you can also manually validate:

email := email.NewEmail().
    SetFrom("sender@example.com").
    AddTo("recipient@example.com").
    SetSubject("Test").
    SetBody("Body")

// Build validates the email
builtEmail, err := email.Build()
if err != nil {
    // Handle validation error
}
3. Use App Passwords (Gmail)

Don't use your regular Gmail password. Generate an App Password:

  1. Enable 2-Step Verification
  2. Go to https://myaccount.google.com/apppasswords
  3. Select "Mail" and generate password
  4. Use the generated password in your config

📊 Advanced Features

Rate Limiting
config := email.SMTPConfig{
    Host:      "smtp.gmail.com",
    RateLimit: 5, // Max 5 emails per second
}
Retry Logic
config := email.SMTPConfig{
    Host:         "smtp.gmail.com",
    MaxRetries:   5,           // Retry up to 5 times
    RetryDelay:   time.Second, // Start with 1s delay
    RetryBackoff: 2.0,         // Double delay each retry (1s, 2s, 4s, 8s, 16s)
}
Context Timeouts
// Timeout after 10 seconds
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

err := mailer.Send(ctx, to, subject, body)

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Inspired by the Go community's best practices
  • Thanks to all contributors

📞 Support

⭐ Star History

If you find this package useful, please consider giving it a star!

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

Examples

Constants

View Source
const (
	DefaultMaxIdleConns    = 2
	DefaultPoolMaxLifetime = 30 * time.Minute
	DefaultPoolMaxIdleTime = 5 * time.Minute
	DefaultMaxMessages     = 100
	DefaultPoolWaitTimeout = 5 * time.Second
)

Pool default constants.

View Source
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

View Source
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")
)
View Source
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")
)
View Source
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.

View Source
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

func BuildRawMessage(e *Email) ([]byte, error)

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

func LoadTemplatesFromDir(dir string, patterns ...string) (map[string]*Template, error)

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

func LoadTemplatesFromFS(fsys fs.FS, patterns ...string) (map[string]*Template, error)

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

func ParseDKIMPrivateKey(pemData []byte) (crypto.Signer, error)

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

func SanitizeHTML(html string) string

SanitizeHTML sanitizes HTML using the default EmailPolicy. It removes elements, attributes, and URL protocols that are unsafe or unsupported in email clients.

func SanitizeHTMLWithPolicy

func SanitizeHTMLWithPolicy(html string, p *Policy) string

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.

func (*AsyncSender) SendWait

func (a *AsyncSender) SendWait(ctx context.Context, e *Email) error

SendWait validates the email eagerly, enqueues it, and blocks until the background worker has finished sending (or the context is cancelled).

type Attachment

type Attachment struct {
	Filename    string
	ContentType string
	Data        []byte
}

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

func (e *Email) AddAttachment(filename, contentType string, data []byte) *Email

AddAttachment adds an attachment. The data slice is copied, so the caller is free to mutate the backing array afterwards.

func (*Email) AddBcc

func (e *Email) AddBcc(bcc ...string) *Email

AddBcc adds BCC recipients

func (*Email) AddCc

func (e *Email) AddCc(cc ...string) *Email

AddCc adds CC recipients

func (*Email) AddHeader

func (e *Email) AddHeader(key, value string) *Email

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) AddTo

func (e *Email) AddTo(to ...string) *Email

AddTo adds recipients

func (*Email) Build

func (e *Email) Build() (*Email, error)

Build validates the email and returns it or an error

func (*Email) SetBody

func (e *Email) SetBody(body string) *Email

SetBody sets the plain text body

func (*Email) SetFrom

func (e *Email) SetFrom(from string) *Email

SetFrom sets the sender

func (*Email) SetHTMLBody

func (e *Email) SetHTMLBody(html string) *Email

SetHTMLBody sets the HTML body

func (*Email) SetReplyTo

func (e *Email) SetReplyTo(replyTo string) *Email

SetReplyTo sets the reply-to address

func (*Email) SetSubject

func (e *Email) SetSubject(subject string) *Email

SetSubject sets the subject

func (*Email) Validate

func (e *Email) Validate() error

Validate validates the email

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

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

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

func NewSlogLogger(logger *slog.Logger) Logger

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 NewMailer

func NewMailer(sender Sender, from string) *Mailer

NewMailer creates a new mailer

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) Close

func (m *Mailer) Close() error

Close closes the mailer

func (*Mailer) RegisterTemplate

func (m *Mailer) RegisterTemplate(name string, tmpl *Template)

RegisterTemplate registers an email template. It is safe for concurrent use.

func (*Mailer) Send

func (m *Mailer) Send(ctx context.Context, to []string, subject, body string) error

Send sends a simple email

func (*Mailer) SendBatch

func (m *Mailer) SendBatch(ctx context.Context, emails []*Email, concurrencyLimit int) error

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.

func (*Mailer) SendEmail

func (m *Mailer) SendEmail(ctx context.Context, email *Email) error

SendEmail sends a custom email

func (*Mailer) SendHTML

func (m *Mailer) SendHTML(ctx context.Context, to []string, subject, html string) error

SendHTML sends an HTML email

func (*Mailer) SendTemplate

func (m *Mailer) SendTemplate(ctx context.Context, to []string, templateName string, data any) error

SendTemplate sends an email using a registered template

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

type Middleware func(Sender) Sender

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) Close

func (m *MockSender) Close() error

Close closes the mock sender

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) Reset

func (m *MockSender) Reset()

Reset clears 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.

func (NoOpLogger) Debug

func (NoOpLogger) Debug(_ string, _ ...any)

Debug implements Logger

func (NoOpLogger) Error

func (NoOpLogger) Error(_ string, _ ...any)

Error implements Logger

func (NoOpLogger) Info

func (NoOpLogger) Info(_ string, _ ...any)

Info implements Logger

func (NoOpLogger) Warn

func (NoOpLogger) Warn(_ string, _ ...any)

Warn implements Logger

func (NoOpLogger) With

func (n NoOpLogger) With(_ ...any) Logger

With implements Logger

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

func (p *Policy) AllowAttributes(element string, attrs ...string) *Policy

AllowAttributes adds allowed attributes for the given element. The element is implicitly added to the allowlist if not already present.

func (*Policy) AllowElements

func (p *Policy) AllowElements(elements ...string) *Policy

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

func (p *Policy) AllowGlobalAttributes(attrs ...string) *Policy

AllowGlobalAttributes adds attributes that are allowed on every element.

func (*Policy) AllowURLProtocols

func (p *Policy) AllowURLProtocols(attr string, protocols ...string) *Policy

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

func (p *Policy) StripElements(elements ...string) *Policy

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.

func (*SMTPSender) Send

func (s *SMTPSender) Send(ctx context.Context, email *Email) error

Send sends an email via SMTP with retry logic

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

func LoadTemplateFromFS(fsys fs.FS, name, path string) (*Template, error)

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

func LoadTemplateFromFile(name, path string) (*Template, error)

LoadTemplateFromFile loads a template from a file. The file content is used as the HTML template.

func NewTemplate

func NewTemplate(name string) *Template

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) Render

func (t *Template) Render(data any) (*Email, error)

Render renders the template with data

func (*Template) SetHTMLTemplate

func (t *Template) SetHTMLTemplate(tmpl string) (*Template, error)

SetHTMLTemplate sets the HTML template

func (*Template) SetSubject

func (t *Template) SetSubject(subject string) *Template

SetSubject sets the subject template

func (*Template) SetTextTemplate

func (t *Template) SetTextTemplate(tmpl string) (*Template, error)

SetTextTemplate sets the plain text template

func (*Template) WithSanitization

func (t *Template) WithSanitization() *Template

WithSanitization enables HTML sanitization on rendered output using the default EmailPolicy. The HTMLBody is sanitized after template rendering.

func (*Template) WithSanitizationPolicy

func (t *Template) WithSanitizationPolicy(p *Policy) *Template

WithSanitizationPolicy enables HTML sanitization on rendered output using a custom Policy.

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.

Directories

Path Synopsis
examples
attachment command
basic command
batch command
middleware command
pool command
template command
providers
mailgun module
otelmail module
sendgrid module
ses module

Jump to

Keyboard shortcuts

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