email

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2025 License: MIT Imports: 13 Imported by: 0

README

email

Lightweight, SMTP-first email toolkit for Go. Batteries included: clean API, templates via fs.FS, multipart (text+HTML), attachments, inline images (CID), connection pooling, timeouts, retries with jitter, and optional rate limiting.

  • Standard library only.
  • Works with embed.FS or disk templates.
  • Designed for production but tiny enough for hobby apps.

Features

  • Context-aware Mailer.Send(ctx, Message, ...Option).
  • Text + HTML multipart, or single-part bodies.
  • Attachments and inline images (Content-ID / cid:).
  • Custom headers, automatic Message-ID, and List-Unsubscribe.
  • SMTP with STARTTLS or implicit TLS (465), timeouts.
  • Connection pooling with health checks and idle TTL.
  • Exponential backoff w/ jitter, transient-error retries.
  • Token-bucket rate limiting (optional, sharable).

Install

go get github.com/aatuh/email/v2

Quick start

package main

import (
  "context"
  "strings"
  "time"

  "github.com/aatuh/email/v2"
  "github.com/aatuh/email/v2/smtp"
  "github.com/aatuh/email/v2/types"
)

func main() {
  ctx := context.Background()

  msg := types.Message{
    From:    types.MustAddr("App <no-reply@example.com>"),
    To:      []types.Address{types.MustAddr("Ada <ada@example.com>")},
    Subject: "Welcome",
    Plain:   []byte("Hi Ada,\n\nWelcome aboard!\n"),
    HTML:    []byte("<p>Hi <b>Ada</b>,</p><p>Welcome aboard!</p>"),
    Attach: []types.Attachment{
      {
        Filename:    "hello.txt",
        ContentType: "text/plain",
        Reader:      strings.NewReader("Hi!"),
      },
    },
  }

  smtp := smtp.NewSMTP(smtp.SMTPConfig{
    Host:        "smtp.example.com",
    Port:        587,
    Username:    "user",
    Password:    "pass",
    Timeout:     10 * time.Second,
    StartTLS:    true,
    PoolMaxIdle: 2,
    PoolIdleTTL: 30 * time.Second,
  })

  bo := email.ExponentialBackoff(
    4,                   // total attempts
    250*time.Millisecond, // base delay
    4*time.Second,        // max backoff
    true,                 // full jitter
  )

  rl := email.NewTokenBucket(10, 10) // 10 msg/s, burst 10

  err := smtp.Send(ctx, msg,
    email.WithListUnsubscribe(
      "<mailto:unsubscribe@example.com>, <https://exmpl/unsub>"),
    email.WithRetry(bo),
    email.WithRateLimit(rl),
  )
  if err != nil {
    panic(err)
  }
}

Templating with fs.FS (supports embed.FS)

Convention:

  • name.txt.tmpl renders the text body.
  • name.html.tmpl renders the HTML body.
  • You may provide one or both.
package main

import (
  "context"
  "embed"
  "time"

  "github.com/aatuh/email/v2"
  "github.com/aatuh/email/v2/smtp"
  "github.com/aatuh/email/v2/types"
)

//go:embed templates/*
var templatesFS embed.FS

func main() {
  ctx := context.Background()

  tpl := email.MustLoadTemplates(templatesFS)
  plain, html, err := tpl.Render("welcome", map[string]any{
    "Name": "Ada",
  })
  if err != nil {
    panic(err)
  }

  msg := types.Message{
    From:    types.MustAddr("App <no-reply@example.com>"),
    To:      []types.Address{types.MustAddr("Ada <ada@example.com>")},
    Subject: "Welcome",
    Plain:   plain,
    HTML:    html,
  }

  smtp := smtp.NewSMTP(smtp.SMTPConfig{
    Host:     "smtp.example.com",
    Port:     587,
    Username: "user",
    Password: "pass",
    Timeout:  10 * time.Second,
    StartTLS: true,
  })

  if err := smtp.Send(ctx, msg); err != nil {
    panic(err)
  }
}

templates/welcome.txt.tmpl:

Hi {{.Name}},

Welcome aboard!

templates/welcome.html.tmpl:

<p>Hi <b>{{.Name}}</b>,</p>
<p>Welcome aboard!</p>

Attachments and inline images (CID)

msg := types.Message{
  // ...
  HTML: []byte(`<p>Logo below:</p><img src="cid:logo">`),
  Attach: []types.Attachment{
    {
      Filename:    "logo.png",
      ContentType: "image/png",
      ContentID:   "logo", // becomes "cid:logo" in HTML
      Reader:      bytes.NewReader(logoBytes),
    },
  },
}

If ContentID is set, the attachment is marked inline and gets a Content-ID header. Otherwise it is a regular attachment.

Headers and unsubscribe

You can set any header on Message.Headers. Common ones are set for you: From, To, Cc, Subject, Date, MIME-Version, Message-ID.

Add List-Unsubscribe per send:

err := smtp.Send(ctx, msg,
  email.WithListUnsubscribe("<mailto:unsub@example.com>, <https://u/x>"),
)

Message.TrackingID adds X-Tracking-ID: ....

Connection pooling and timeouts

Enable pooling via SMTPConfig:

smtp := smtp.NewSMTP(smtp.SMTPConfig{
  Host:        "smtp.example.com",
  Port:        587,
  Username:    "user",
  Password:    "pass",
  Timeout:     10 * time.Second,
  StartTLS:    true,
  PoolMaxIdle: 4,              // keep up to 4 idle connections
  PoolIdleTTL: 60 * time.Second, // close idle after 60s
})

The pool performs a simple NOOP health check when reusing connections.

Timeouts:

  • SMTPConfig.Timeout applies to dial and I/O.
  • A context deadline takes precedence if provided to Send.

STARTTLS vs implicit TLS (465)

  • Use StartTLS: true for submission ports like 587.
  • Use ImplicitTLS: true for port 465.
// Port 465:
smtp := smtp.NewSMTP(smtp.SMTPConfig{
  Host:        "smtp.example.com",
  Port:        465,
  Username:    "user",
  Password:    "pass",
  Timeout:     10 * time.Second,
  ImplicitTLS: true,
})

SkipVerify exists for local dev only. Do not use it in production.

Retries and backoff with jitter

bo := email.ExponentialBackoff(
  5,                    // total attempts (initial + 4 retries)
  200*time.Millisecond, // base
  5*time.Second,        // cap
  true,                 // full jitter
)

err := smtp.Send(ctx, msg, email.WithRetry(bo))

Transient errors (timeouts, 4xx, temporary network issues) are retried. Non-transient errors return immediately.

Rate limiting

To prevent bursts, share a token bucket across sends or across workers:

bucket := email.NewTokenBucket(5, 10) // 5 msg/s, burst 10
err := smtp.Send(ctx, msg, email.WithRateLimit(bucket))

API reference (brief)

// Package types
type Address struct {
  Name string
  Mail string
}
func MustAddr(s string) types.Address
func ParseAddress(s string) (types.Address, error)
func ParseAddressList(list []string) ([]types.Address, error)

type Attachment struct {
  Filename    string
  ContentType string
  ContentID   string
  Reader      io.Reader
}

type Message struct {
  From       types.Address
  To, Cc, Bcc []types.Address
  Subject    string
  Plain      []byte
  HTML       []byte
  Attach     []types.Attachment
  Headers    map[string]string
  TrackingID string
}
func (m *types.Message) Validate() error

// Package email
type Mailer interface {
  Send(ctx context.Context, msg types.Message, opts ...Option) error
}

type Option func(*SendConfig)
func WithListUnsubscribe(v string) Option
func WithRetry(b Backoff) Option
func WithRateLimit(bucket *TokenBucket) Option
func WithPool(pool *ConnPool) Option

type Backoff interface {
  Next(i int) (time.Duration, bool)
}
func ExponentialBackoff(
  attempts int, base, max time.Duration, fullJitter bool,
) Backoff

type TokenBucket struct { /* ... */ }
func NewTokenBucket(rate float64, burst int) *TokenBucket

type TemplateSet struct { /* ... */ }
func MustLoadTemplates(fsys fs.FS) *TemplateSet
func LoadTemplates(fsys fs.FS) (*TemplateSet, error)
func (t *TemplateSet) Render(name string, data any) ([]byte, []byte, error)

// Package smtp
type SMTPConfig struct {
  Host        string
  Port        int
  Username    string
  Password    string
  LocalName   string
  Timeout     time.Duration
  StartTLS    bool
  ImplicitTLS bool
  SkipVerify  bool
  PoolMaxIdle int
  PoolIdleTTL time.Duration
}

func NewSMTP(cfg smtp.SMTPConfig) *smtp.SMTP

Error handling

Send returns descriptive errors for SMTP phases:

  • smtp auth: ...
  • smtp MAIL FROM: ...
  • smtp RCPT TO <addr>: ...
  • smtp DATA: ...
  • smtp write: ...
  • smtp end data: ...

Use context.WithTimeout to bound total send time. When retries are enabled, the total wall time equals the sum of backoff delays plus the final attempt duration.

Security notes

  • Prefer STARTTLS or implicit TLS with certificate verification on.
  • Do not store credentials in code. Use env vars or secrets managers.
  • Consider outbound rate limits.

Roadmap

  • Optional DKIM signing.
  • Hooks for metrics/tracing (OpenTelemetry) without new deps.

Documentation

Overview

Package email provides a lightweight, SMTP-first email toolkit for Go. It includes clean API, templates via fs.FS, multipart (text+HTML), attachments, inline images (CID), connection pooling, timeouts, retries with jitter, and optional rate limiting.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Backoff

type Backoff interface {
	// Next returns sleep before attempt i (0-based). ok=false when no more.
	Next(i int) (d time.Duration, ok bool)
}

Backoff describes retry sleep schedule.

func ExponentialBackoff

func ExponentialBackoff(
	attempts int,
	base time.Duration,
	max time.Duration,
	fullJitter bool,
) Backoff

ExponentialBackoff returns a Backoff with jitter. attempts is total tries (>=1). base is initial delay, max caps delay. fullJitter picks [0,d) vs half-jitter [d/2, d).

Parameters:

  • attempts: The total tries.
  • base: The initial delay.
  • max: The max delay.
  • fullJitter: The full jitter.

Returns:

  • Backoff: The backoff.

type ConnPool

type ConnPool struct {
	MaxIdle   int
	IdleTTL   time.Duration
	New       func() (any, error)
	Close     func(any) error
	IsHealthy func(any) bool
	// contains filtered or unexported fields
}

ConnPool is a simple sized pool for client connections. It stores opaque connections. Adapter owns the concrete type.

The pool is safe for concurrent use.

func NewConnPool

func NewConnPool(
	maxIdle int,
	idleTTL time.Duration,
	newFn func() (any, error),
	closeFn func(any) error,
	isHealthyFn func(any) bool,
) *ConnPool

NewConnPool creates a new pool.

Parameters:

  • maxIdle: The maximum number of idle connections.
  • idleTTL: The idle timeout.
  • newFn: The new function.
  • closeFn: The close function.
  • isHealthyFn: The is healthy function.

Returns:

  • *ConnPool: The new pool.

func (*ConnPool) CloseAll

func (p *ConnPool) CloseAll()

CloseAll drains the pool and closes all idle connections.

func (*ConnPool) Get

func (p *ConnPool) Get() (any, error)

Get returns a connection from pool or creates one.

Returns:

  • any: The connection.
  • error: An error if the connection creation fails.

func (*ConnPool) Put

func (p *ConnPool) Put(conn any)

Put returns a connection to the pool.

Parameters:

  • conn: The connection to return to the pool.

type Mailer

type Mailer interface {
	// Send sends an email message with the given options.
	//
	// Parameters:
	//   - ctx: The context for cancellation and timeouts.
	//   - msg: The email message to send.
	//   - opts: Optional configuration for this send operation.
	//
	// Returns:
	//   - error: An error if the email fails to send. Implementations
	//     should return context errors when the operation is cancelled
	//     or times out.
	Send(ctx context.Context, msg types.Message, opts ...Option) error
}

Mailer defines the interface for email sending adapters. Implementations should handle connection management, authentication, and delivery according to their specific protocol (SMTP, API, etc.).

type Option

type Option func(*SendConfig)

Option configures per-send behavior.

func WithDKIM

func WithDKIM(cfg types.DKIMConfig) Option

WithDKIM enables DKIM signing using the provided config.

Parameters:

  • cfg: The DKIM config.

Returns:

  • Option: The option.

func WithHooks

func WithHooks(h *types.Hooks) Option

WithHooks attaches observability hooks (OTel-friendly, no deps).

Parameters:

  • h: The hooks.

Returns:

  • Option: The option.

func WithListUnsubscribe

func WithListUnsubscribe(v string) Option

WithListUnsubscribe sets the List-Unsubscribe header.

Parameters:

  • v: The List-Unsubscribe header value.

Returns:

  • Option: The option.

func WithPool

func WithPool(pool *ConnPool) Option

WithPool sets a connection pool to reuse adapter connections.

Parameters:

  • pool: The connection pool.

Returns:

  • Option: The option.

func WithRateLimit

func WithRateLimit(bucket *TokenBucket) Option

WithRateLimit attaches a token bucket for throttling.

Parameters:

  • bucket: The token bucket.

Returns:

  • Option: The option.

func WithRetry

func WithRetry(b Backoff) Option

WithRetry configures a retry backoff. Nil disables retries.

Parameters:

  • b: The retry backoff.

Returns:

  • Option: The option.

type SendConfig

type SendConfig struct {
	ListUnsub string
	Backoff   Backoff
	Rate      *TokenBucket
	Pool      *ConnPool
	Hooks     *types.Hooks
	DKIM      *types.DKIMConfig
}

SendConfig is applied during Send.

type TemplateSet

type TemplateSet struct {
	// contains filtered or unexported fields
}

TemplateSet loads and renders text and HTML templates from an fs.FS.

Convention:

name.txt.tmpl  -> plain text body
name.html.tmpl -> HTML body

Both files are optional; at least one must exist to render a message.

func LoadTemplates

func LoadTemplates(fsys fs.FS) (*TemplateSet, error)

LoadTemplates walks fsys and parses *.txt.tmpl and *.html.tmpl.

Parameters:

  • fsys: The filesystem.

Returns:

  • *TemplateSet: The template set.
  • error: The error if the template set fails to load.

func MustLoadTemplates

func MustLoadTemplates(fsys fs.FS) *TemplateSet

MustLoadTemplates panics on error; useful for init.

Parameters:

  • fsys: The filesystem.

Returns:

  • *TemplateSet: The template set.

func (*TemplateSet) Render

func (t *TemplateSet) Render(name string, data any) ([]byte, []byte, error)

Render renders "name" by locating "name.txt.tmpl" and "name.html.tmpl" anywhere in the parsed set. If only one exists, the other return is nil.

Parameters:

  • name: The name of the template.
  • data: The data to render the template with.

Returns:

  • []byte: The plain text body.
  • []byte: The HTML body.
  • error: The error if the template fails to render.

type TokenBucket

type TokenBucket struct {
	// contains filtered or unexported fields
}

TokenBucket is a simple thread-safe token bucket.

func NewTokenBucket

func NewTokenBucket(rate float64, burst int) *TokenBucket

NewTokenBucket returns a token bucket generating "rate" tokens per second with a capacity of "burst".

Parameters:

  • rate: The rate of tokens per second.
  • burst: The max tokens.

Returns:

  • *TokenBucket: The token bucket.

func (*TokenBucket) Wait

func (tb *TokenBucket) Wait()

Wait blocks until one token is available.

Parameters:

  • tb: The token bucket.

Returns:

  • void: The token bucket is blocked until one token is available.

Directories

Path Synopsis
Package smtp is a mailer adapter for SMTP.
Package smtp is a mailer adapter for SMTP.

Jump to

Keyboard shortcuts

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