mailer

package module
v0.0.0-...-51f860f Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: MIT Imports: 4 Imported by: 0

README

mailer

A small, backend-agnostic transactional email library for Go.

import (
    "github.com/PS-safe/mailer"
    "github.com/PS-safe/mailer/brevo"
)

sender := brevo.New(os.Getenv("BREVO_API_KEY"))

err := sender.Send(ctx, mailer.Message{
    From:    mailer.Address{Name: "My App", Email: "app@example.com"},
    To:      []mailer.Address{{Email: "user@example.com"}},
    Subject: "Verify your email",
    Text:    "Your code is 123456",
    HTML:    "<p>Your code is <b>123456</b></p>",
})

Swap brevo.New(...) for memory.New() in tests or smtp.New(...) in prod — the Sender interface is all the calling code sees.

What it gets right

  • One interface, three backends. Sender has a single method, Send. Calling code never knows which backend it's talking to.
  • Zero dependencies. Pure standard library — net/smtp, net/http, mime/quotedprintable. Nothing to audit, nothing to keep up to date.
  • Validation at the boundary. Every backend runs Message.Validate() before any network work; a malformed message fails fast with ErrInvalidMessage instead of a confusing provider error.
  • Errors never leak the upstream body. Brevo error payloads have echoed api-key fragments and account IDs. The brevo backend returns the status code only — a library shouldn't log, and it shouldn't hand you a string that's unsafe to log either.
  • The memory backend is a real test double. It validates exactly like a real backend, captures messages for assertions via Sent(), and can simulate failure with FailNext().
  • multipart/alternative done properly. When a message has both Text and HTML, the smtp backend emits a quoted-printable multipart body so clients pick the format they want and UTF-8 survives intact.

API surface

type Address struct { Name, Email string }

type Message struct {
    From    Address
    To      []Address
    Subject string
    Text    string // plain-text body
    HTML    string // optional HTML body
}

func (m Message) Validate() error // wraps ErrInvalidMessage

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

Backends

Backend Import Status Use for
memory mailer/memory ✅ complete tests, local dev — captures, sends nothing
smtp mailer/smtp ✅ complete any SMTP server; local catchers (MailHog/Mailpit); providers once you have SMTP creds
brevo mailer/brevo ✅ complete free transactional path — verifies a single sender by email link, no domain needed
Why Brevo specifically

Of the free-tier providers, Brevo is the only one that lets you send to arbitrary recipients after verifying just one sender address by email link — no DNS domain ownership required. Resend, SendGrid, Mailgun, Postmark, and SES sandbox all require a verified domain to reach anyone but yourself. Once you own a domain, add a resend backend and migrate; the Sender interface means calling code doesn't change.

cmd/sendmail

The runnable demo is a CLI, not an HTTP server — a mailer is something you call, not something you POST to.

# zero config: the memory backend captures and prints, sends nothing
MAIL_FROM=app@example.com go run ./cmd/sendmail -to you@example.com -subject hi -text hello

# real send through Brevo
MAILER_BACKEND=brevo BREVO_API_KEY=xkeysib-... MAIL_FROM=verified@gmail.com \
  go run ./cmd/sendmail -to someone@example.com -subject "hi" -text "hello"

Configuration (env)

Variable Backend Purpose
MAILER_BACKEND all memory (default), smtp, or brevo
MAIL_FROM all sender email — required
MAIL_FROM_NAME all sender display name — optional
BREVO_API_KEY brevo API key
SMTP_HOST / SMTP_PORT smtp server address
SMTP_USERNAME / SMTP_PASSWORD smtp PLAIN auth; omit both for an unauthenticated relay

Design decisions worth noting

  • Send takes a context.Context. The brevo backend honors it fully (http.NewRequestWithContext). net/smtp has no context-aware dial, so the smtp backend honors cancellation at the boundary only — documented, not hidden.
  • The library never logs. A Send either returns nil or an error. Logging and retry policy belong to the caller, who has the request context the library doesn't.
  • Validate owns the error wrapping. It returns errors that already wrap ErrInvalidMessage, so every backend is just if err := m.Validate(); err != nil { return err } — the sentinel contract can't be forgotten.
  • No Cc/Bcc/attachments in v0. They're real, but they widen Message and every backend's encoding. Shipped the 90% case first; see roadmap.

Local dev

go test ./...
MAIL_FROM=a@b.com go run ./cmd/sendmail -to c@d.com -text hi

Future work

  • Cc / Bcc / Reply-To
  • Attachments
  • resend backend (once a verified domain is in play)
  • AWS SES backend
  • Context-aware SMTP dial (manual DialContext + STARTTLS)
  • Template helper — render Text + HTML from one source
  • MailHog-based integration test for the smtp backend (testcontainers)

License

MIT

Documentation

Overview

Package mailer defines a backend-agnostic transactional email interface.

One Sender interface, multiple backends:

memory — captures messages in-process (tests, dev)
smtp   — any SMTP server (net/smtp); the portable standard
brevo  — Brevo HTTP API; free transactional path that accepts a
         single email-verified sender without owning a domain

The library never logs and never sends partial output: a Send either returns nil or an error. Callers own logging and retry policy.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidMessage is returned by Send when m fails Validate.
	ErrInvalidMessage = errors.New("mailer: invalid message")

	// ErrSendFailed wraps a backend delivery failure. Backends wrap this
	// with a status code or transport error — never the upstream response
	// body, which can leak credentials.
	ErrSendFailed = errors.New("mailer: send failed")
)

Functions

This section is empty.

Types

type Address

type Address struct {
	Name  string
	Email string
}

Address is a named email address. Name is optional; Email is required.

type Message

type Message struct {
	From    Address
	To      []Address
	Subject string
	Text    string // plain-text body
	HTML    string // optional HTML body
}

Message is a single transactional email. At least one of Text or HTML must be set; a backend sends multipart/alternative when both are present.

func (Message) Validate

func (m Message) Validate() error

Validate reports whether m is well-formed enough to attempt delivery. Backends call this before doing any network work and return its result directly — every non-nil error wraps ErrInvalidMessage, so callers can errors.Is against that sentinel while still seeing the specific reason. This is the boundary check; backends trust the message past this point.

type Sender

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

Sender is the contract every backend satisfies. Implementations MUST be safe for concurrent use.

Directories

Path Synopsis
Package brevo is a Sender backed by the Brevo (formerly Sendinblue) transactional email HTTP API.
Package brevo is a Sender backed by the Brevo (formerly Sendinblue) transactional email HTTP API.
cmd
sendmail command
Command sendmail sends one message through a mailer backend chosen at runtime.
Command sendmail sends one message through a mailer backend chosen at runtime.
Package memory is an in-process Sender for tests and local dev.
Package memory is an in-process Sender for tests and local dev.
Package smtp is a Sender backed by any SMTP server (net/smtp).
Package smtp is a Sender backed by any SMTP server (net/smtp).

Jump to

Keyboard shortcuts

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