| Environment | Backend | Where messages go |
|---|---|---|
| Local development | SMTP | The maildev container in docker-compose.yml, inbox at http://localhost:1080 |
cppal-dev |
SMTP | The in-cluster maildev pod, inbox at https://www.cppal-dev.boost.org/maildev/ |
stage |
SMTP | The in-cluster maildev pod, inbox at https://www.stage.boost.org/maildev/ |
production |
Mailgun (Anymail) | Real recipients |
| Tests | locmem |
django.core.mail.outbox, see config/test_settings.py |
Non-production environments deliberately send nothing to real recipients. This makes it safe to test flows against real user accounts (sign-in verification, moderation notices, subscription confirmations) without those users receiving anything, and it keeps QA traffic off the Mailgun sender domain.
Mailman is not affected by any of this. It is driven through MAILMAN_REST_API_URL, not Django's email backend.
- Set
CATCH_ALL_EMAIL=trueandX_DEPLOYMENT_ENV=<env's label>inkube/boost/values-stage-gke.yamlandkube/boost/values-cppal-dev-gke.yaml. - When enabled,
EMAIL_BACKENDdefaults to Django's SMTP backend pointed atEMAIL_HOST/EMAIL_PORT(maildev:1025) instead of Mailgun. TheMAILGUN_*values in those files stay present but become inert. - Django raises
ImproperlyConfiguredat startup if this is enabled while the environment looks like production, by either signal:X_DEPLOYMENT_ENVisproduction, orENVIRONMENT_NAMEisProduction Environment. Two signals, so neither one being unset or renamed is on its own enough to let catch-all through. Mis-routing production email should be loud, not silent. - Default is
false, so an environment only changes behavior if its values file opts in. Enable it only where amaildevpod is also deployed (maildevInstall: true), otherwise sends will fail with a connection error.
Open the URL for the environment and enter the basic-auth credentials:
stage: https://www.stage.boost.org/maildev/cppal-dev: https://www.cppal-dev.boost.org/maildev/
Credentials come from the maildev-auth Secret in that namespace, which is created by hand and is not in this repository:
kubectl -n stage create secret generic maildev-auth \
--from-literal=web_user='<user>' \
--from-literal=web_pass='<password>'The pod will not start until that Secret exists. Ask an operator for the credentials rather than reading them out of the cluster.
Things worth knowing before someone reports them as bugs:
- The inbox is ephemeral. maildev holds messages in memory, so a pod restart (including every deploy that rolls it) empties the inbox. There is no retention by design.
- The inbox updates live. New messages appear without refreshing, over a websocket.
- Do not publish the URL. maildev applies basic auth as Express middleware, but its socket.io channel is attached to the raw HTTP server and bypasses that middleware, so the live message feed is readable by anyone who can reach the path. The password gate deters casual access; it is not a security boundary. The inbox contains real user addresses and working sign-in links for the QA database.
kube/boost/templates/maildev.yamlholds everything, behindmaildevInstall: theDeployment, aClusterIPService, and (for Gateway environments) anHTTPRoute, aHealthCheckPolicyand aGCPBackendPolicy.- Django reaches SMTP at
maildev:1025over cluster DNS. That port is never exposed outside the cluster. - The inbox is published as a
/maildevpath on the environment'smainFqdn, via a secondHTTPRouteon the existing Gateway. It reuses the existing static IP and certificate, so no DNS record or certificate is involved, and the traffic never reaches Django or the app pods. MAILDEV_BASE_PATHNAME=/maildevmakes maildev serve itself under that prefix, so no URL rewriting is needed at the edge. The socket.io endpoint moves under the same prefix and is covered by the same route.- The
HealthCheckPolicytargets/maildev/healthz, the only path maildev exempts from basic auth. A health check against/would get a 401, which the load balancer reads as an unhealthy backend. - The
GCPBackendPolicyraisestimeoutSec. On Google load balancers the backend timeout is the maximum lifetime of a websocket connection rather than an idle timeout, and the 30 second default would sever the live-update socket every 30 seconds. replicasmust stay at 1. Messages live in the pod's memory, so a second replica would split the inbox.
- In that environment's values file, set
maildevInstall: trueand addCATCH_ALL_EMAIL: "true"(plusEMAIL_HOST: maildevandEMAIL_PORT: "1025") to theEnvlist. - Create the
maildev-authSecret in that namespace before deploying. - The route is only rendered for environments using the GKE Gateway (
gatewayType: "gce"). Elsewhere you get the pod and the SMTP sink, and the inbox is reachable withkubectl -n <ns> port-forward svc/maildev 1080:1080.