Skip to content

(Story #2554) [WIP] Route QA email to a shared maildev inbox - #2555

Draft
herzog0 wants to merge 4 commits into
developfrom
teo/2554-maildev-k8s
Draft

(Story #2554) [WIP] Route QA email to a shared maildev inbox#2555
herzog0 wants to merge 4 commits into
developfrom
teo/2554-maildev-k8s

Conversation

@herzog0

@herzog0 herzog0 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Ref #2554

Why

Non-production deployments send real email to real recipients. config/settings.py hardcoded EMAIL_BACKEND to the Mailgun Anymail backend for every environment except LOCAL_DEVELOPMENT, and both values-stage-gke.yaml and values-cppal-dev-gke.yaml supply live Mailgun credentials and a verified sender domain.

That blocks QA work which needs to sign in as existing users with real contribution history, because the sign-in verification email is delivered to that user's actual inbox. More broadly, any transactional email triggered while testing reaches real recipients, consumes email quota, and generates bounces and spam signals against the sender domain.

Local development already solves this with the maildev container in docker-compose.yml. This brings the same catch-all inbox to stage and cppal-dev, reachable in a browser so QA does not need cluster credentials.

What changed

config/settings.py - a new CATCH_ALL_EMAIL flag (default false) makes an environment send through Django's SMTP backend to EMAIL_HOST/EMAIL_PORT instead of Mailgun. Django raises ImproperlyConfigured at startup if the flag is ever enabled while X_DEPLOYMENT_ENV is production.

The flag is opt-in per values file rather than derived from the environment name. Deriving it (from X_DEPLOYMENT_ENV, or by pattern-matching DJANGO_FQDN) would silently switch environments that have no maildev pod, turning working mail into connection errors, and would re-enable real sending if a hostname were ever renamed.

kube/boost/templates/maildev.yaml (new) - everything behind a maildevInstall flag, following the existing redisInstall / celeryInstall convention so production cannot pick it up:

  • Deployment (pinned maildev/maildev:2.2.1, replicas: 1) and a ClusterIP Service exposing SMTP 1025 and HTTP 1080.
  • For Gateway environments only: an HTTPRoute publishing the inbox at /maildev/ on the environment's mainFqdn, plus a HealthCheckPolicy and a GCPBackendPolicy.

Values files - maildevInstall: true and CATCH_ALL_EMAIL: "true" in the two QA files; maildevInstall: false plus maildevImageTag as chart defaults. Production is untouched. The MAILGUN_* entries stay in the QA files but are inert, so reverting is a one-line change.

Docs - new docs/email.md (routing per environment, how to reach the inbox, how it is wired), plus CATCH_ALL_EMAIL / EMAIL_* entries in docs/env_vars.md and an index line in docs/README.md.

How the inbox is exposed

The route is added at the existing GKE Gateway, so the Google load balancer that already fronts the site gains one URL-map rule. It reuses the existing static IP and certificate: no new hostname, no DNS record, no certificate, no LoadBalancer, and no change to the app's nginx config. Traffic to /maildev/ never reaches Django, gunicorn or the app pods.

Access is gated by maildev's own HTTP basic auth (MAILDEV_WEB_USER / MAILDEV_WEB_PASS).

Three details that are load-bearing, all verified against maildev/maildev:2.2.1:

  • MAILDEV_BASE_PATHNAME=/maildev makes maildev serve itself under the prefix, so no URL rewriting is needed at the edge, and the socket.io endpoint moves under the same prefix where the PathPrefix rule already covers it.
  • The HealthCheckPolicy targets /maildev/healthz, the only path maildev exempts from basic auth. A health check against / returns 401, which the load balancer reads as an unhealthy backend and answers with 503.
  • The GCPBackendPolicy raises timeoutSec. On Google load balancers the backend timeout is the maximum lifetime of a WebSocket connection rather than an idle timeout, so the 30 second default would sever the inbox's live-update socket every 30 seconds.

Deploy notes

A maildev-auth Secret must exist in the namespace before deploying, or the pod will not start. It is not in the repo because this repository is public:

kubectl -n stage create secret generic maildev-auth \
  --from-literal=web_user='<user>' --from-literal=web_pass='<password>'

The values-cppal-dev-gke.yaml change only takes effect once the cppalliance/website-v2-qa fork's cppal-dev branch picks up this commit.

Two things to confirm on the first deploy: that the Gateway controller auto-attached a NEG to the maildev Service (otherwise add a cloud.google.com/neg annotation), and that the GCPBackendPolicy CRD is present (kubectl get crd | grep gcpbackendpolicies; HealthCheckPolicy from the same GKE bundle is already used in gateway.yaml).

Verification

helm template across all four values files:

  • production: no maildev objects at all, Mailgun backend unchanged.
  • stage and cppal-dev: Deployment, Service, HTTPRoute, HealthCheckPolicy and GCPBackendPolicy all render; HTTPRoute resolves to www-boost-stage / www-boost-dev on the correct hostname; CATCH_ALL_EMAIL, EMAIL_HOST and EMAIL_PORT reach all five mail-sending containers (boost/wsgi, boost/nginx, celery-worker, celery-beat) and the migrations Job.
  • Chart defaults (values.yaml): no maildev objects.

Settings behavior, exercised in the project image:

Configuration Result
LOCAL_DEVELOPMENT=true SMTP backend, maildev:1025
No flag, X_DEPLOYMENT_ENV=production Mailgun backend with MAILGUN_* in ANYMAIL
Flag on, X_DEPLOYMENT_ENV=stage or dev SMTP backend, maildev:1025, empty ANYMAIL
Flag on, X_DEPLOYMENT_ENV=production ImproperlyConfigured at startup

End to end: with Django configured as the chart configures it and a real maildev:2.2.1 container as the SMTP host, send_mail(...) to realuser@example.com was captured by maildev rather than delivered, and read back through the authenticated API. Basic auth, the auth-exempt health path, relative asset resolution under the prefix and a real 101 Switching Protocols upgrade at /maildev/socket.io/ were all confirmed against the pinned image.

pre-commit run passes on all changed files.

Not included

  • A dedicated hostname for the inbox (would need a DNS record and a Certificate Manager certmap entry).
  • Per-person authentication (SSO / oauth2-proxy) or IP allowlisting.
  • Message retention across pod restarts; maildev holds messages in memory by design.

Known limitation

maildev applies basic auth as Express middleware, but its socket.io channel attaches to the raw HTTP server and bypasses that middleware, emitting full message payloads. The password gate deters casual access to the UI; it is not a security boundary for message contents. The URL should not be published. Accepted for a QA inbox.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 540501dc-7484-495f-bb62-d9fab2c31f76

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch teo/2554-maildev-k8s

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@herzog0
herzog0 force-pushed the teo/2554-maildev-k8s branch from ba2d090 to df43eae Compare August 4, 2026 18:46
@herzog0
herzog0 force-pushed the teo/2554-maildev-k8s branch from 980345f to c4ee485 Compare August 4, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Spike + Task] Route all outbound email in QA environments to a shared MailDev inbox with a password-protected web UI

1 participant