Help us shape the future of Go! Take the 2026 Go Developer Survey

backplane-smtp

module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT

README

backplane-smtp

Durable e-mail delivery for a Backplane installation: one small service that does one job. A product never imports it — it raises a hook or publishes an event, and the operator wires that to smtp.Send in the console, with the installation's own SMTP accounts.

What it offers the installation

Kind Name What
Activity smtp.Send Queues a rendered mail (to, subject, text/html, cc, bcc, reply_to, headers, attachments, optional account, from, idempotency_key) and answers {id, status, account, suppressed[], duplicate} at once. The relay delivers it with retries; id is the mail's Message-ID.
Hook smtp.Route Optional: the installation picks the account and sender per message ({account, from, to, subject, candidates[]} → {account, from}). Unbound: the message's account, else default_account.
Events smtp.Sent, smtp.Failed, smtp.Suppressed Facts of every delivery for other services' rules. Failed is permanent: the relay refused for good or the attempts ran out.
Reactor smtp-failed-suppressor A recipient the relay rejected for good is not sent to again for suppression_ttl.
Workflow smtp.Report Counts the queue by status and the active suppressions and leaves them as an audit record (smtp.report); schedule it from the console for a periodic line in the history. Needs Temporal; without it the workflow is declared and never runs.
Console /s/smtp/* The service's own pages: overview (queue by status, live settings), accounts with a reachability test, the queue (requeue or discard a dead delivery), suppressions (lift one). Backed by the internal smtp.console.v1.AdminService, relayed under the operator's session; envelopes only, never bodies. Operator actions are audit records (smtp.account.tested, smtp.delivery.requeued, smtp.delivery.discarded, smtp.suppression.lifted).

Every outcome is also a platform audit record (event.name smtp.delivered, smtp.failed, smtp.suppressed; subject message/<id>; recipients hashed), so the console's Audit is the delivery journal; the service keeps no bodies. Infrastructure failures are errors (CaptureError), not facts.

How it works

Send validates the message, drops suppressed recipients, picks the account, then in one PostgreSQL transaction claims the idempotency key and enqueues the delivery in a pg-outbox queue. Every replica runs the relay: a claimed delivery is built into MIME and handed to its account (a pool and a rate per account); a 5xx reply discards it and reports Failed, anything else is retried with backoff up to outbox.max_attempts, after which it is dead and reported Failed (exhausted). The same key within idempotency_ttl returns the earlier delivery and its outcome instead of a second mail.

State: the service's schema smtp in PostgreSQL — the outbox (pg-outbox's table, declared in the service's own schema so one migration set owns the schema), idempotency, suppression — migrated at start, compacted hourly.

Configuration

A file (BACKPLANE_CONFIG_FILE) or SMTP_* environment variables; the SDK block is BACKPLANE_* (reference).

accounts:
  main:
    kind: smtp                  # or log: mails go to the log (development)
    host: smtp.example.com
    port: 587
    tls: starttls               # starttls | tls | none
    username: shop
    password: ${SMTP_PASSWORD}  # secret
    from: Shop <no-reply@shop.example>
    pool: 4                     # deliveries in flight on this account
    rate_per_minute: 600
    timeout: 30s
default_account: main           # live
dry_run: false                  # live: record deliveries, send nothing
suppressions_enabled: true      # live
max_message_bytes: 26214400
max_recipients: 100
idempotency_ttl: 168h
suppression_ttl: 720h
outbox: {concurrency: 4, max_attempts: 10, retry_max: 10m, retention: 72h, dead_retention: 168h}
store:
  dsn: postgres://smtp:secret@postgres:5432/backplane?sslmode=disable

Live fields change from the console without a restart. The store is required: without it the instance is not ready.

Development

make configure          # linter, sqld and proto generators into ./bin
make configure-web      # the console module's packages (GitHub Packages, see web/README.md)
make gen                # internal API: Go stubs and the module's TypeScript client from proto/
make test               # unit tests; PostgreSQL tests skip without BACKPLANE_TEST_PG
make test-live          # against platform-in-a-box's PostgreSQL (backplane: make up)
make web                # build the console module (embedded by make build)
make run                # against the local stack with deployments/dev.yaml
make db                 # regenerate internal/store/db from schema.sql and queries

The release binary and image carry the console module; a binary built without make web serves the internal API alone and declares no UI.

Tests run a real SMTP server in process (internal/accounts/smtptest) and the SDK's component harness; the relay, the queue and the suppressor are exercised against PostgreSQL.

Directories

Path Synopsis
cmd
smtp command
smtp is a Backplane tool service: it delivers rendered mail through the SMTP accounts of an installation, durably, and tells the platform what happened.
smtp is a Backplane tool service: it delivers rendered mail through the SMTP accounts of an installation, durably, and tells the platform what happened.
internal
accounts
Package accounts holds the installation's SMTP accounts: their configuration, a sender per account (a real relay or the log, for development) and the limits each one applies.
Package accounts holds the installation's SMTP accounts: their configuration, a sender per account (a real relay or the log, for development) and the limits each one applies.
accounts/smtptest
Package smtptest is an SMTP server for tests: it keeps what it receives and refuses the recipients it is told to.
Package smtptest is an SMTP server for tests: it keeps what it receives and refuses the recipients it is told to.
admin
Package admin is smtp's internal API: what the console module reads and does, served as gRPC on the platform port behind the internal secret and relayed by backplane under the operator's session.
Package admin is smtp's internal API: what the console module reads and does, served as gRPC on the platform port behind the internal secret and relayed by backplane under the operator's session.
flows
Package flows is smtp's Temporal side: the Report workflow an administrator may schedule (the service's only Temporal-needing part; without Temporal it is declared and never runs) and its activity.
Package flows is smtp's Temporal side: the Report workflow an administrator may schedule (the service's only Temporal-needing part; without Temporal it is declared and never runs) and its activity.
mail
Package mail is the service's domain: the message a caller hands to smtp.Send, its checks and limits, address hashing and the MIME it becomes.
Package mail is the service's domain: the message a caller hands to smtp.Send, its checks and limits, address hashing and the MIME it becomes.
mailer
Package mailer is the service's work: the smtp.Send activity that queues a mail, the Route hook an installation may bind to pick the account, the delivery itself when the queue hands it back, the events and audit records of every outcome, and the suppressor reacting to permanent failures.
Package mailer is the service's work: the smtp.Send activity that queues a mail, the Route hook an installation may bind to pick the account, the delivery itself when the queue hands it back, the events and audit records of every outcome, and the suppressor reacting to permanent failures.
outbox
Package outbox is the durable queue between smtp.Send and the relay: gopherex/pg-outbox over the service's store.
Package outbox is the durable queue between smtp.Send and the relay: gopherex/pg-outbox over the service's store.
store
Package store is the service's PostgreSQL: the SDK's postgres provider with the service's schema (smtp) and migrations — its own, which also declare pg-outbox's table — plus the queries sqld generated and an hourly compaction of what expires.
Package store is the service's PostgreSQL: the SDK's postgres provider with the service's schema (smtp) and migrations — its own, which also declare pg-outbox's table — plus the queries sqld generated and an hourly compaction of what expires.
ui
Package ui carries the console module: web/dist/plugin copied here by `make web` and embedded into the binary.
Package ui carries the console module: web/dist/plugin copied here by `make web` and embedded into the binary.
proto

Jump to

Keyboard shortcuts

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