Skip to main content

Email relay

info

This summary page explains what the email relay stack is, how the series fits together, and how to get started.

Email relay series

  1. Email relay - You are here
  2. Architecture
  3. Manifests
  4. Flux integration
  5. Operations

What this is

A shared Kubernetes-based email relay infrastructure that provides MX domain validation, email logging, and Microsoft 365 Graph API delivery. Applications in the cluster send email through this relay instead of directly to external SMTP servers.

The problem I solved

Self-hosted scheduling applications attract spam bookings with fake email addresses. Someone could flood my calendar with appointments using spammer@nonexistent-domain.xyz and I would never know until the confirmation emails silently failed.

Microsoft Security Defaults also blocks legacy SMTP AUTH, so I needed a relay that uses modern authentication (Graph API) anyway.

Instead of solving these separately, I built a single relay that validates recipient domains have MX records before any email enters the system, logs all emails for debugging, and delivers via Microsoft 365 Graph API.

Components

ComponentVersionPortPurpose
mx-validatorlatest25SMTP proxy with MX validation + ICS stripping
Mailpitv1.28.31025, 8025SMTP logging, web UI for debugging
smtp2graphv1.1.42525Microsoft 365 Graph API delivery

All three containers run in a single pod, communicating via localhost.

Architecture overview

Protection provided

AttackProtectionResult
Fake domain bookingsMX record validationRejected at SMTP level
Typo domains (gmial.com)MX record validationRejected (no MX)
Bounce stormsEarly rejectionNo emails sent
Resource exhaustionCached MX lookups, early rejectMinimal resources used
Sender reputation damageInvalid emails never sentM365 reputation preserved
Microsoft Graph ICS routing bugICS calendar strippingEmails routed correctly

Repository structure

The stack spans two repositories following the standard GitOps pattern.

App repo

Contains the Kubernetes manifests for the email-relay deployment.

your-org/email-relay/
├── .sops.yaml # SOPS encryption config
└── k8s/prod/
├── kustomization.yaml
├── 00-namespace.yaml
├── 10-secret-mailpit-auth.enc.yaml
├── 15-secret-smtp2graph-config.enc.yaml
├── 20-deployment.yaml # 3-container pod
├── 25-pvc-mailpit.yaml # Persistent storage for email logs
├── 30-service.yaml
├── 40-networkpolicy.yaml
├── 50-servicemonitor.yaml
└── 80-prometheusrule.yaml

Flux config repo

Contains the Flux objects that deploy the app repo.

clusters/my-cluster/email-relay/
├── kustomization.yaml
├── source.yaml # GitRepository
├── 00-kustomization-ns.yaml
├── 10-kustomization-app.yaml
└── prod/
├── kustomization.yaml
└── image-automation.yaml # Auto-update mx-validator

Quick start

After deploying via Flux:

# Check Flux status
flux get kustomizations -n flux-system | grep email-relay

# Check pods
kubectl get pods -n email-relay

# View mx-validator logs
kubectl logs -n email-relay -l app=email-relay -c mx-validator -f

# View Mailpit UI (all logged emails)
kubectl port-forward -n email-relay svc/email-relay 8025:8025
# Open http://localhost:8025

# Check Prometheus metrics
kubectl exec -n email-relay -l app=email-relay -c mx-validator -- \
wget -q -O- http://localhost:9090/metrics | grep mx_validator

Using the relay from applications

Applications connect to the relay using the internal service name:

env:
- name: EMAIL_SERVER_HOST
value: "email-relay.email-relay.svc.cluster.local"
- name: EMAIL_SERVER_PORT
value: "25"

See Cal.com scheduling for a complete integration example.

What you will learn

  • Architecture: how the three containers work together, email flow, and attack scenarios protected
  • Manifests: the mx-validator Go code, Dockerfile, and Kubernetes manifests
  • Flux integration: GitOps deployment with image automation for the custom mx-validator
  • Operations: Azure App Registration, smtp2graph config, Prometheus metrics, and troubleshooting