Postmark-compatible API
The same endpoints and JSON as Postmark: /email, batches, templates. Existing SDKs work by changing the base URL.
v0.1.0 · open source · one static Go binary
postmarkd is a small daemon that sits next to your apps, on a server or as a container sidecar, and delivers their mail through Postmark. Use HTTP, SMTP, sendmail or the CLI. Your Postmark token stays in one place.
docker run -e POSTMARKD_BLACKHOLE=true -p 8025:8025 ghcr.io/rossedman/postmarkd:0.1# Body from stdin, sent through Postmark $ df -h | postmarkd send -to ops@example.com \ -subject "Disk usage on web-1" b7bc2f4a-e38e-4336-af7d-e6c392c2f817 # Templates, streams, attachments $ postmarkd send -to ada@example.com \ -template welcome -model '{"name":"Ada"}'
# Make every program on the host use Postmark $ sudo ln -s /usr/bin/postmarkd /usr/sbin/sendmail # cron, mailx, PHP mail(), git… just work $ echo "Job ran" | mail -s "Nightly job" root@example.com $ printf 'Subject: Hi\n\nHello\n' | sendmail ada@example.com exit 0
# Point any SMTP app at localhost. No TLS, no password. EMAIL_HOST = "127.0.0.1" # Django EMAIL_PORT = 2525 # Postmark headers work over SMTP too X-PM-Message-Stream: broadcast X-PM-Tag: newsletter
# Postmark's own API, over a unix socket $ curl --unix-socket /run/postmarkd/postmarkd.sock \ http://localhost/email -d '{ "To": "ada@example.com", "Subject": "Your receipt", "TextBody": "Thanks!" }' {"MessageID":"b7bc2f4a-…","ErrorCode":0,"Message":"OK"}
How it works
Whatever your software speaks, postmarkd turns it into the same validated, rate-limited Postmark API call, over pooled connections and with retries that never send a message twice.
mail, PHP mail(), gitDKIM-signed and delivered to the inbox
Features
The same endpoints and JSON as Postmark: /email, batches, templates. Existing SDKs work by changing the base URL.
A local SMTP listener and a drop-in sendmail. MIME, attachments, inline images and X-PM-* headers handled.
Accept everything, send nothing, answer like Postmark. Tests read back what was “sent”. No account, no network.
Receipts on outbound, newsletters on broadcast, per message or per request, even mixed in one batch.
500-message batches, a concurrency limit, pooled keep-alive connections and a bounded queue with back-pressure.
postmarkd check-domain tells you which DNS records are missing, and the exact values to add.
Socket activation (even port 25, unprivileged), readiness, watchdog, graceful drain and a hardened unit.
A 16 MB non-root image with a built-in health check. Compose, Kubernetes and CI examples included.
Prometheus metrics for every endpoint and Postmark call, health and readiness checks, structured logs.
Benchmarked against an instant fake Postmark, so these numbers are postmarkd’s own overhead. See the benchmarks.
Install
# Debian / Ubuntu (for Fedora, RHEL: dnf install the .rpm; see the docs) $ VERSION=0.1.0 ARCH=$(dpkg --print-architecture) $ REL=https://github.com/rossedman/postmarkd/releases/download/v$VERSION $ curl -fsSLO $REL/postmarkd_${VERSION}_linux_$ARCH.deb $ sudo apt install ./postmarkd_${VERSION}_linux_$ARCH.deb $ sudo install -m 0600 /dev/stdin /etc/postmarkd/server-token <<< 'your-server-token' $ sudo systemctl enable --now postmarkd $ echo "Hello from $(hostname)" | postmarkd send -to you@example.com -subject "It works"
$ docker run -d --name postmarkd \ -p 127.0.0.1:8025:8025 -p 127.0.0.1:2525:2525 \ -e POSTMARKD_SERVER_TOKEN=your-server-token \ -e POSTMARKD_DEFAULT_FROM=noreply@yourdomain.com \ ghcr.io/rossedman/postmarkd:0.1 $ curl localhost:8025/email -d '{"To":"you@example.com","Subject":"Hi","TextBody":"Hello"}'
# A native sidecar: starts before your app, stops after it. initContainers: - name: postmarkd image: ghcr.io/rossedman/postmarkd:0.1 restartPolicy: Always env: - { name: POSTMARKD_LISTEN, value: 127.0.0.1:8025 } - name: POSTMARKD_SERVER_TOKEN valueFrom: { secretKeyRef: { name: postmark, key: server-token } }
# GitHub Actions: a fake Postmark for your integration tests services: postmarkd: image: ghcr.io/rossedman/postmarkd:0.1 env: { POSTMARKD_BLACKHOLE: "true" } ports: [ "8025:8025", "2525:2525" ] # then, in a test step: $ curl -s localhost:8025/blackhole/messages | jq '.Messages[].Message.Subject'
Why postmarkd
For one app with a Postmark SDK, you should. postmarkd helps when mail comes from many places, or from places that can’t easily talk to Postmark.
| Postmark directly | With postmarkd | |
|---|---|---|
| Server token | Copied into every app, container and CI secret | In one place. Apps get nothing, or their own revocable token |
cron, PHP mail(), mailx | Configure Postfix as an authenticated TLS relay | Symlink postmarkd as sendmail |
| SMTP-only apps | Postmark's SMTP host, TLS and credentials in each | localhost:2525, nothing else |
| Tests and CI | Test token: needs network, can’t read back mail | Blackhole mode: offline, assert on what was sent |
| Latency | Every send waits on the Postmark round trip | Local socket, or ?async=true returns immediately |
| Defaults and rules | Repeated in each app | Set once for the host |
Free, open source and dependency-free. Read the docs, or try it right now in blackhole mode. No Postmark account needed.