mailer is a production-oriented Go library for asynchronous, concurrent email delivery. It combines a buffered queue, a worker-pool runtime, context-aware lifecycle control, validated message construction, and a pluggable transport (with a batteries-included, TLS-capable SMTP backend) so you can offload email sending from your request path without blocking.
flowchart LR
P["Producers<br/>(HTTP handlers, jobs)"]
B["MailContentBuilder<br/>validate & build"]
Q(["Buffered queue<br/>cap = QueueSize"])
subgraph Pool["Worker pool (WorkerCount)"]
W1["worker 1"]
W2["worker 2"]
WN["worker N"]
end
T{{"MailerService<br/>Send(ctx, MailContent)"}}
SMTP["MailerSMTP<br/>(TLS / STARTTLS)"]
CUS["Custom backend<br/>(SES, SendGrid, …)"]
P -->|"NewMailContentBuilder()"| B
B -->|"MailContent"| P
P -->|"Enqueue() — blocks when full"| Q
Q --> W1 & W2 & WN
W1 & W2 & WN --> T
T --> SMTP
T --> CUS
classDef q fill:#fde68a,stroke:#b45309,color:#000;
classDef t fill:#bfdbfe,stroke:#1d4ed8,color:#000;
class Q q;
class T t;
- Concurrent worker pool — configurable number of workers drain a shared queue.
- Back-pressure — a bounded, buffered queue;
Enqueueblocks (or fails on context cancellation) when full instead of growing unbounded. - Graceful shutdown —
Stop()closes the queue, drains in-flight work, and waits for workers. Context cancellation stops workers immediately. - Pluggable transports — implement the small
MailerServiceinterface to send through any provider;MailContentexposes read accessors so external backends work. - TLS-capable SMTP — implicit TLS (SMTPS, port 465) and opportunistic/required STARTTLS, PLAIN auth, configurable dial timeout and EHLO name.
- Validated, injection-safe content — a fluent
MailContentBuildervalidates addresses, MIME type, and lengths, and rejects CR/LF/NUL in header fields (SMTP header-injection protection). - Observability — structured
log/sloglogging and typed, wrappable errors (errors.Is/errors.As). - Zero third-party dependencies — standard library only.
- Go 1.25 or newer.
go get github.com/slashdevops/mailer@latestimport "github.com/slashdevops/mailer"package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/slashdevops/mailer"
)
func main() {
// 1. Configure a transport (the built-in SMTP backend).
smtpMailer, err := mailer.NewMailerSMTP(mailer.MailerSMTPConf{
SMTPHost: os.Getenv("SMTP_HOST"),
SMTPPort: 587,
Username: os.Getenv("SMTP_USER"),
Password: os.Getenv("SMTP_PASS"),
RequireTLS: true, // refuse to send over an unencrypted connection
})
if err != nil {
slog.Error("invalid SMTP configuration", "error", err)
os.Exit(1)
}
// 2. Bind the worker context to OS signals for graceful shutdown.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// 3. Create and start the queue-backed service.
service, err := mailer.NewMailService(&mailer.MailServiceConfig{
Ctx: ctx,
WorkerCount: 4,
QueueSize: 256,
Mailer: smtpMailer,
})
if err != nil {
slog.Error("failed to create mail service", "error", err)
os.Exit(1)
}
service.Start()
// 4. Build a validated message and enqueue it.
content, err := mailer.NewMailContentBuilder().
WithFromName("Operations").
WithFromAddress("ops@example.com").
WithToName("Customer").
WithToAddress("customer@example.com").
WithMimeType(mailer.MimeTypeTextPlain).
WithSubject("Hello from mailer").
WithBody("This is a queued email.").
Build()
if err != nil {
slog.Error("invalid email content", "error", err)
os.Exit(1)
}
if err := service.Enqueue(content); err != nil {
slog.Error("failed to enqueue email", "error", err)
}
// 5. Shut down gracefully: drain the queue and wait for workers.
<-ctx.Done()
service.Stop()
}A complete, runnable program lives in example/main.go.
| Type | Responsibility |
|---|---|
MailContentBuilder / MailContent |
Build and validate an immutable message. |
MailService |
Queue messages and dispatch them through a worker pool. |
MailerService (interface) |
Transport contract: Send(ctx, MailContent) error. |
MailerSMTP |
Standard-library SMTP transport with TLS/STARTTLS and auth. |
| Field | Default | Description |
|---|---|---|
Ctx |
context.Background() |
Governs worker lifetime; cancel to stop workers. |
WorkerCount |
— (required, 1–100) | Number of concurrent workers. |
QueueSize |
WorkerCount |
Buffered queue capacity (burst absorption). |
Timeout |
0 (disabled) |
Per-message deadline applied to each Send. |
Mailer |
— (required) | The transport implementation. |
| Field | Default | Description |
|---|---|---|
SMTPHost / SMTPPort |
— (required) | Server host and port (1–65535). |
Username / Password |
empty | PLAIN auth credentials; empty disables auth. |
ImplicitTLS |
false (implied on 465) |
TLS from the first byte (SMTPS). |
RequireTLS |
false |
Fail rather than send over an unencrypted link. |
TLSConfig |
verify against SMTPHost, TLS 1.2+ |
Custom *tls.Config. |
DialTimeout |
10s |
TCP connection timeout. |
LocalName |
localhost |
Name announced in EHLO/HELO. |
sequenceDiagram
autonumber
actor App as Application
participant Svc as MailService
participant Q as Queue (channel)
participant W as Worker
participant M as MailerService (SMTP)
participant Srv as Mail server
App->>Svc: Start()
Svc->>W: spawn WorkerCount goroutines
App->>Svc: Enqueue(content)
alt queue has room
Svc->>Q: content
Svc-->>App: nil
else queue full
Note over Svc,Q: Enqueue blocks (back-pressure)<br/>until a slot frees or ctx is cancelled
end
W->>Q: receive content
W->>M: Send(ctx, content)
M->>Srv: dial + (STARTTLS/TLS) + AUTH + DATA
Srv-->>M: 250 OK
M-->>W: nil / *MailerError
App->>Svc: Stop()
Svc->>Q: close()
W->>Q: drain remaining
W-->>Svc: workers exit
Svc-->>App: returns after drain
stateDiagram-v2
[*] --> Ready: NewMailService()
Ready --> Running: Start()
Running --> Running: Enqueue() / Send()
Running --> Draining: Stop()
Draining --> Stopped: queue drained, workers exited
Running --> Cancelled: ctx cancelled
Cancelled --> Stopped: workers exit (no drain)
Stopped --> [*]
note right of Draining
graceful: queued
messages are sent
end note
note right of Cancelled
hard stop: in-flight
sends observe ctx.Done()
end note
Start()is idempotent and launchesWorkerCountgoroutines.Enqueue()is safe for concurrent use. It blocks while the queue is full and returnsErrServiceStoppedafterStop(), or the context error if the context is cancelled first.Stop()is idempotent and safe from multiple goroutines. It stops accepting work, closes the queue, and drains queued messages before returning.- Cancelling
Ctxstops workers promptly without draining; use it for hard shutdown andStop()for graceful shutdown.
MailContent exposes read accessors (FromName(), FromAddress(), ToName(), ToAddress(), MimeType(), Subject(), Body()), so any backend can implement MailerService:
type ConsoleMailer struct{}
func (ConsoleMailer) Send(ctx context.Context, m mailer.MailContent) error {
slog.Info("would send", "to", m.ToAddress(), "subject", m.Subject())
return nil
}See docs/examples/custom-backend.md.
- Docs overview
- Production guide — lifecycle, TLS, worker sizing, observability, testing.
- Basic example
- Custom backend example
- GoDoc reference
make test # go test -race with coverage
make lint # golangci-lint
make cover # enforce the coverage thresholdSee CONTRIBUTING.md and DEVELOPMENT_GUIDELINES.md.
Report vulnerabilities per SECURITY.md. The builder rejects header-injection attempts (CR/LF/NUL in header fields), and the SMTP backend supports RequireTLS to avoid sending credentials or content over unencrypted connections.
Licensed under the Apache License 2.0. See LICENSE.