etampe is a small SMTP-to-Cloudflare Email Sending bridge for homelab Kubernetes clusters. It accepts SMTP on port 2525, parses the message into Cloudflare's REST payload, sends through the Cloudflare Email Sending API, exposes /healthz, /readyz, and /metrics on port 8080, and can export OpenTelemetry traces and metrics over OTLP.
Prerequisites:
- A domain onboarded to Cloudflare Email Service (SPF, DKIM, and bounce MX records configured).
- A Cloudflare API token with the Account / Email Service: Edit permission scoped to the target account.
Required:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN: API token with Account / Email Service: Edit permission.
Common options:
CLOUDFLARE_FROM: optional sender override for verified Cloudflare senders.SMTP_USERNAMEandSMTP_PASSWORD: enable SMTP AUTH PLAIN when both are set.SMTP_ALLOW_INSECURE_AUTH: defaulttruefor private cluster networks; use TLS or NetworkPolicy when SMTP auth is enabled.SMTP_ADDR: default:2525.HTTP_ADDR: default:8080.OTEL_EXPORTER_OTLP_ENDPOINT: enables OTLP trace and metric export.
Notes:
- Cloudflare
429and5xxresponses are returned to SMTP clients as temporary451failures; sender queues should retry. - Prometheus and OTLP metrics are both emitted. Avoid ingesting both into the same backend unless you de-duplicate.
- SMTP certificates are loaded at startup; restart the pod after Kubernetes secret rotation.
- SMTP
RCPT TOis authoritative.To:andCc:recipients not present in the envelope are stripped from the outbound Cloudflare payload, envelope recipients missing from visible headers are sent asbcc, and if no visible recipient remains the first envelope recipient is promoted toto.
docker run --rm -p 2525:2525 -p 8080:8080 \
-e CLOUDFLARE_ACCOUNT_ID=... \
-e CLOUDFLARE_API_TOKEN=... \
ghcr.io/jfroy/etampe:latestCreate a secret:
kubectl create secret generic etampe \
--from-literal=CLOUDFLARE_ACCOUNT_ID=... \
--from-literal=CLOUDFLARE_API_TOKEN=...Apply a private ClusterIP deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: etampe
spec:
selector:
matchLabels:
app: etampe
template:
metadata:
labels:
app: etampe
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: etampe
image: ghcr.io/jfroy/etampe:latest
envFrom:
- secretRef:
name: etampe
ports:
- name: smtp
containerPort: 2525
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /readyz
port: http
livenessProbe:
httpGet:
path: /healthz
port: http
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
---
apiVersion: v1
kind: Service
metadata:
name: etampe
spec:
selector:
app: etampe
ports:
- name: smtp
port: 2525
targetPort: smtp
- name: http
port: 8080
targetPort: httpPoint in-cluster apps at etampe.default.svc.cluster.local:2525. Scrape http://etampe:8080/metrics.