mailer

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 6 Imported by: 0

README

go-mailer

Email sending for Go with pluggable providers — Amazon SES and Mailgun behind a single Message model, typed errors and composable retry/failover.

Go Reference CI

Why

Provider SDKs lock your application code to one vendor: mailgun-go speaks only Mailgun, the AWS SDK is raw API surface, go-mail is SMTP-focused. go-mailer is the thin layer in between — you write against one interface, and switching providers (or running two with failover) doesn't touch application code. Failures come back as typed errors (ErrAuth, ErrRateLimited, ErrRejected, ...) that match with errors.Is regardless of the provider.

Install

go get github.com/YusufDrymz/go-mailer

Usage

sender := mailgun.New("mg.example.com", os.Getenv("MAILGUN_API_KEY"))

res, err := sender.Send(ctx, &mailer.Message{
    From:    "Orders <orders@example.com>",
    To:      []string{"customer@example.com"},
    Subject: "Your invoice",
    Text:    "Invoice attached.",
    HTML:    "<p>Invoice attached.</p>",
    Attachments: []mailer.Attachment{
        {Filename: "invoice.pdf", ContentType: "application/pdf", Content: pdfBytes},
    },
})

Amazon SES uses your existing AWS config (profile, role or env vars):

cfg, err := config.LoadDefaultConfig(ctx) // aws-sdk-go-v2/config
sender := ses.New(cfg)

Plain text/HTML goes through SES Simple content; attachments or custom headers switch to a raw MIME message built with the standard library. Bcc never leaks into the message headers — blind copies ride on the API destination only.

Retry and failover

Both wrap any Sender, so they compose:

// retry Mailgun on transient errors (429/5xx/network), then switch to SES
sender := mailer.Failover(
    mailer.Retry(mg, mailer.WithAttempts(3)),
    ses.New(cfg),
)

Retry uses exponential backoff with full jitter and only fires on errors where a retry can help (mailer.IsRetryable). Failover skips the fallback only for invalid messages, which would fail everywhere anyway. Note that the SES provider already retries throttling inside the AWS SDK; tune that on aws.Config instead of stacking Retry on top.

Error handling
res, err := sender.Send(ctx, msg)
switch {
case errors.Is(err, mailer.ErrInvalidMessage): // caught before any network call
case errors.Is(err, mailer.ErrAuth):           // bad API key / AWS credentials
case errors.Is(err, mailer.ErrRateLimited):    // 429, SES throttling or quota
case errors.Is(err, mailer.ErrRejected):       // provider refused the message
case errors.Is(err, mailer.ErrProviderDown):   // 5xx / network, retryable
}

var e *mailer.Error
if errors.As(err, &e) {
    log.Printf("provider=%s code=%s retryable=%t", e.Provider, e.Code, e.Retryable)
}

Testing your integration

Everything is injectable, no interfaces to mock: point Mailgun at an httptest.Server with mailgun.WithBaseURL(srv.URL), and SES with ses.WithBaseEndpoint(srv.URL). That's exactly how this package tests itself.

FAQ

EU Mailgun domain? mailgun.New(domain, key, mailgun.WithBaseURL(mailgun.EUBaseURL)).

Templates, bulk send, webhooks? Out of scope — this is a sending layer. Queueing/persistence also stays out; compose with your own queue if you need delivery guarantees beyond a synchronous send.

Which provider sent my message? Result.Provider — useful behind Failover.

🇹🇷 Türkçe

Amazon SES ve Mailgun'ı tek Message modeli ve tek Sender interface'i arkasında toplayan e-posta gönderim katmanı. Sağlayıcı değiştirmek uygulama kodunu değiştirmez; iki sağlayıcıyı Failover ile yedekli de kullanabilirsiniz.

Kurulum: go get github.com/YusufDrymz/go-mailer

mg := mailgun.New("mg.example.com", apiKey)
sender := mailer.Failover(mailer.Retry(mg), ses.New(awsCfg))
res, err := sender.Send(ctx, &mailer.Message{
    From: "Sipariş <orders@example.com>", To: []string{"musteri@example.com"},
    Subject: "Faturanız", Text: "Fatura ektedir.",
})

Hatalar sağlayıcıdan bağımsız typed error olarak döner: ErrAuth, ErrRateLimited, ErrRejected, ErrProviderDown (errors.Is ile aile, errors.As ile ham kod). Attachment/özel header SES tarafında stdlib ile kurulan raw MIME'a geçer; Bcc header'lara asla sızmaz. Retry yalnızca geçici hatalarda exponential backoff + jitter uygular.

License

MIT — see LICENSE.

Documentation

Overview

Package mailer sends email through pluggable providers (Amazon SES, Mailgun) behind a single Message model and a one-method Sender interface.

Provider failures surface as typed errors you can match with errors.Is (ErrAuth, ErrRateLimited, ErrRejected, ...) regardless of which provider produced them. Retry and Failover wrap any Sender, so "try SES, fall back to Mailgun" is a one-liner instead of application code.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidMessage = &Error{Kind: KindInvalid, Message: "invalid message"}
	ErrAuth           = &Error{Kind: KindAuth, Message: "authentication failed"}
	ErrRateLimited    = &Error{Kind: KindRateLimited, Message: "rate limited", Retryable: true}
	ErrRejected       = &Error{Kind: KindRejected, Message: "message rejected"}
	ErrProviderDown   = &Error{Kind: KindProviderDown, Message: "provider unavailable", Retryable: true}
)

Sentinel errors for matching with errors.Is.

Functions

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether err is a provider error worth retrying. Retry uses it; it is exported because application-level queues want the same decision.

Types

type Attachment

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

Attachment is an in-memory file attached to a Message. ContentType falls back to application/octet-stream when empty.

type Error

type Error struct {
	Provider  string
	Code      string
	Message   string
	Retryable bool
	Kind      Kind
}

Error is a send failure. Provider is "mailgun" or "ses"; Code carries the raw provider signal (HTTP status or AWS error code). Match families with errors.Is (e.g. errors.Is(err, mailer.ErrRateLimited)) and read the concrete details with errors.As.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches by family, so any provider's 429 satisfies ErrRateLimited.

type Kind

type Kind int

Kind groups provider errors into families so callers can match behavior without caring which provider produced the error.

const (
	KindOther Kind = iota
	KindInvalid
	KindAuth
	KindRateLimited
	KindRejected
	KindProviderDown
)

type Message

type Message struct {
	From    string
	To      []string
	Cc      []string
	Bcc     []string
	ReplyTo string
	Subject string
	Text    string
	HTML    string

	Attachments []Attachment

	// Headers are extra message headers (e.g. "X-Campaign-ID"). Providers that
	// cannot send a header transparently switch to their raw/MIME path.
	Headers map[string]string

	// Tags map to Mailgun o:tag values and SES message tags. SES restricts
	// tag names to [a-zA-Z0-9_-].
	Tags []string
}

Message is a provider-independent email. Text or HTML (or both) must be set; everything else is optional. Addresses accept both bare ("a@example.com") and display-name ("Ayşe <a@example.com>") forms.

func (*Message) Validate

func (m *Message) Validate() error

Validate checks the minimum shape every provider needs. Providers call it before any network work, so a broken message fails fast with ErrInvalidMessage instead of a provider round-trip.

type Result

type Result struct {
	MessageID string
	Provider  string
}

Result reports a successful send. MessageID is the provider-assigned id and its format differs per provider; Provider tells which sender actually delivered (useful behind Failover).

type RetryOption

type RetryOption func(*retrySender)

RetryOption configures Retry.

func WithAttempts

func WithAttempts(n int) RetryOption

WithAttempts sets the total number of attempts (default 3).

func WithBaseDelay

func WithBaseDelay(d time.Duration) RetryOption

WithBaseDelay sets the first backoff delay (default 500ms). Each retry doubles it up to the max delay.

func WithMaxDelay

func WithMaxDelay(d time.Duration) RetryOption

WithMaxDelay caps the backoff delay (default 10s).

type Sender

type Sender interface {
	Send(ctx context.Context, m *Message) (*Result, error)
}

Sender sends a single message. Implementations: mailgun.Client, ses.Client, and the Retry / Failover wrappers.

func Failover

func Failover(primary Sender, fallbacks ...Sender) Sender

Failover tries senders in order and returns the first success. It moves on for any failure except an invalid message, which would fail on every provider anyway. All collected errors are joined, so errors.Is still matches families on the combined error.

Combine with Retry per provider when you want "retry primary a few times, then switch": Failover(Retry(primary), fallback).

func Retry

func Retry(next Sender, opts ...RetryOption) Sender

Retry wraps next so retryable errors (see IsRetryable) are retried with exponential backoff and full jitter. Non-retryable errors return immediately. Note that the SES provider already retries throttling at the AWS SDK layer; stacking Retry on top multiplies attempts.

Directories

Path Synopsis
examples
send command
Sends one message through Mailgun, falling back to Amazon SES when Mailgun is unreachable.
Sends one message through Mailgun, falling back to Amazon SES when Mailgun is unreachable.
Package mailgun implements mailer.Sender on the Mailgun HTTP API (v3) with no dependency beyond the standard library.
Package mailgun implements mailer.Sender on the Mailgun HTTP API (v3) with no dependency beyond the standard library.
Package ses implements mailer.Sender on Amazon SES v2 using the official AWS SDK.
Package ses implements mailer.Sender on Amazon SES v2 using the official AWS SDK.

Jump to

Keyboard shortcuts

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