Skip to content
lattiqPublic

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Mailer - Go Email Library

Go Reference Go Report Card License: MIT

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.

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

Installation

go get github.com/lattiq/mailer

Quick Start

package 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!")
}

Providers

AWS SES

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"),
)

SendGrid

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithSendGrid("your-api-key"),
)

Mailgun

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"),
)

SMTP

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", "587", "username", "password", false),
)

The SMTP provider upgrades the connection with STARTTLS (port 587). Implicit TLS on port 465 is not supported.

Dry Run

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,
    }),
)

Advanced Configuration

Retry Logic

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithRetry(3, 100*time.Millisecond, 5*time.Second, 2.0),
    mailer.WithJitter(true),
)

Rate Limiting

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithRateLimit(100, time.Minute, 10), // 100 emails per minute, burst of 10
    mailer.WithPerRecipientRateLimit(true),
)

Circuit Breaker

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithCircuitBreaker(5, 3, 60*time.Second), // 5 failures trigger, 3 successes to close, 60s timeout
)

Fallback Provider

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithAWSSES("us-east-1"),
    mailer.WithFallbackProvider(mailer.ProviderSendGrid, mailer.ProviderSettings{
        "api_key": "your-sendgrid-key",
    }),
)

Template Support

Setup Templates

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

Template Structure

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.

Send Template Email

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)

Inline Images

Embed an image in the email instead of linking to it, so it displays without the recipient clicking "download pictures" (Outlook blocks remote images by default). Attach it with Inline: true and reference it from the template with the cid function:

<img src="{{cid "logo"}}" width="150" height="40" alt="Example" />
//go:embed logo.png
var logoPNG []byte

err := client.SendTemplate(ctx, &mailer.TemplateRequest{
    Template: "otp",
    // ...
    Attachments: []mailer.Attachment{{
        Filename:    "logo.png",
        ContentType: "image/png",
        Data:        bytes.NewReader(logoPNG),
        Inline:      true,
        ContentID:   "logo",
    }},
})
  • Use {{cid ...}} rather than writing cid: into the template data: html/template treats cid: as unsafe and replaces it with #ZgotmplZ.
  • Pass a seekable reader such as bytes.NewReader. Retries rewind and re-read it; a non-seekable reader would be empty on a retry.
  • Mailgun uses the Content-ID as the uploaded file name and infers the image type from it, so keep the extension (ContentID: "logo.png", referenced as {{cid "logo.png"}}).
  • Attachments without Inline are sent as regular downloadable attachments. Both kinds work on every provider; SES switches to SendRawEmail when an email has attachments or headers.

Priority and Custom Headers

email.Priority = mailer.PriorityHigh                  // adds X-Priority: 2, Importance: high
email.Headers = map[string]string{"X-Category": "otp"}
  • PriorityHigh and PriorityUrgent add X-Priority and Importance headers on every provider. PriorityLow and PriorityNormal add nothing: PriorityLow is the zero value, so it cannot be told apart from an unset priority.
  • A custom header with the same name overrides the priority headers.
  • Custom headers must not contain line breaks and cannot override headers the mailer sets itself (From, To, Cc, Bcc, Subject, Date, MIME-Version, Content-Type, Content-Transfer-Encoding). Such emails fail validation.
  • Non-ASCII subjects and header values are encoded automatically.

Batch Operations

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)

Development

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 sast          # SAST: govulncheck + gosec (high severity)
make sbom          # CycloneDX SBOM (syft) + vulnerability scan (grype, fails on High)
make ci            # everything above: run before every commit
make fix           # format, lint --fix, and go mod tidy
make patch         # bump patch version and create a git tag

Run make help to see the full target list.

Run make ci before committing: GitHub Actions (.github/workflows/ci.yaml) runs the same targets on every push and PR, plus the tests on the minimum Go version in go.mod. make sbom needs syft and grype (brew install syft grype). govulncheck also reports vulnerabilities in the Go standard library you build with, so keep your Go toolchain on the latest patch release.

Observability

Distributed Tracing

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithTracing("my-service", "1.0.0", 1.0),
    mailer.WithTracingEndpoint("http://jaeger:14268/api/traces"),
)

Metrics

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithMetrics("myapp_mailer", 30*time.Second),
)

Logging

client, err := mailer.New(
    mailer.DefaultConfig(),
    mailer.WithLogging("info", "json", "stdout"),
    mailer.WithRequestResponseLogging(false), // Be careful with sensitive data
)

Error Handling

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 Validation

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

Send and SendTemplate validate automatically. Validation checks the addresses, requires a subject and a body, and rejects line breaks in the subject or custom headers (header injection) and custom headers that override mailer-set ones.

Performance Considerations

  • 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

Security

  • 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

License

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

Examples

For more examples, see the examples/ directory:

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages