Skip to main content

Operations

info

This page covers Azure App Registration setup, smtp2graph configuration, Prometheus metrics, and troubleshooting.

Email relay series

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

Azure App Registration

The smtp2graph component requires an Azure App Registration with Mail.Send permission to send emails via Microsoft Graph API.

Create the App Registration

Portal: https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade

  1. New registration

    • Name: Email Relay
    • Supported account types: "Accounts in this organizational directory only"
    • Click Register
  2. Copy IDs (from Overview page):

    • Application (client) ID
    • Directory (tenant) ID
  3. Create Client Secret:

    • Go to Certificates & secretsNew client secret
    • Description: smtp2graph
    • Expires: 730 days (24 months)
    • Copy the Value immediately (shown only once)
  4. Add API Permission:

    • Go to API permissionsAdd a permission
    • Select Microsoft GraphApplication permissions
    • Search for Mail.Send → Select it → Add permissions
    • Click Grant admin consent for [tenant]

smtp2graph configuration

Create the config secret with your Azure credentials:

# k8s/prod/15-secret-smtp2graph-config.enc.yaml (before encryption)
apiVersion: v1
kind: Secret
metadata:
name: smtp2graph-config
namespace: email-relay
type: Opaque
stringData:
config.yml: |
mode: full
send:
appReg:
tenant: <your-tenant-id>
id: <your-client-id>
secret: <your-client-secret>
retryLimit: 3
retryInterval: 5
receive:
port: 2525
banner: "Email Relay"
maxSize: 25m
ipWhitelist:
- 127.0.0.1
allowedFrom:
- you@yourdomain.com

Encrypt before committing:

sops -e -i k8s/prod/15-secret-smtp2graph-config.enc.yaml

Prometheus metrics

MX Validator metrics

MetricTypeDescription
mx_validator_accepted_totalCounterEmails with valid MX records
mx_validator_rejected_totalCounterEmails rejected (no MX)
mx_validator_connections_totalCounterTotal SMTP connections
mx_validator_active_connectionsGaugeCurrent active connections
mx_validator_ics_stripped_totalCounterEmails with ICS calendar attachments stripped

Mailpit metrics

MetricTypeDescription
mailpit_messagesGaugeMessages in database
mailpit_smtp_accepted_totalCounterSMTP messages accepted
mailpit_smtp_rejected_totalCounterSMTP messages rejected

Example Grafana queries

# Rejection rate per minute
rate(mx_validator_rejected_total[5m]) * 60

# Acceptance rate per minute
rate(mx_validator_accepted_total[5m]) * 60

# Total emails processed
mx_validator_accepted_total + mx_validator_rejected_total

# Rejection percentage
mx_validator_rejected_total / (mx_validator_accepted_total + mx_validator_rejected_total) * 100

# Messages waiting in Mailpit
mailpit_messages

# ICS stripping rate
rate(mx_validator_ics_stripped_total[5m]) * 60

Alerting rules

The PrometheusRule in Manifests defines:

AlertConditionSeverity
MXValidatorHighRejectionRate>1 rejection/min for 5mWarning
EmailRelayDownPod not respondingCritical
MailpitHighMessageCount>1000 messagesWarning

Accessing Mailpit UI

Mailpit provides a web UI for viewing all emails that pass through the relay.

Via port-forward

kubectl port-forward -n email-relay svc/email-relay 8025:8025
# Open http://localhost:8025

Via ingress (optional)

Create an ingress for internal network access:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mailpit-ingress
namespace: email-relay
spec:
ingressClassName: nginx
tls:
- hosts:
- mailpit.example.local
rules:
- host: "mailpit.example.local"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: email-relay
port:
number: 8025

Verification commands

Check pods

kubectl get pods -n email-relay
# Expected:
# email-relay-xxxxx 3/3 Running 0 ...

Check logs

# MX validator logs (shows accepted/rejected)
kubectl logs -n email-relay -l app=email-relay -c mx-validator -f

# Mailpit logs (shows relayed messages)
kubectl logs -n email-relay -l app=email-relay -c mailpit -f

# smtp2graph logs (shows Graph API delivery)
kubectl logs -n email-relay -l app=email-relay -c smtp2graph -f

Check metrics

# MX validator metrics
kubectl exec -n email-relay deployment/email-relay -c mx-validator -- \
wget -q -O- http://localhost:9090/metrics | grep mx_validator

# Mailpit metrics
kubectl exec -n email-relay deployment/email-relay -c mx-validator -- \
wget -q -O- http://localhost:9091/metrics | grep mailpit

Test MX validation

# Check if domain has MX records
dig +short MX example.com

# Test SMTP connection
kubectl run test-smtp --rm -it --restart=Never --image=busybox -- \
sh -c "echo -e 'EHLO test\nQUIT' | nc email-relay.email-relay.svc.cluster.local 25"

Verify ICS stripping

# Check if ICS stripping is enabled
kubectl logs -n email-relay -l app=email-relay -c mx-validator --tail=20 | grep "ICS stripping"

# Check ICS stripped count
kubectl exec -n email-relay deployment/email-relay -c mx-validator -- \
wget -q -O- http://localhost:9090/metrics | grep ics_stripped

# Compare attachments in Mailpit
# Emails to STRIP_ICS_RECIPIENTS should have 0 attachments
# Emails to other recipients should retain ICS attachments

Troubleshooting

Emails not sending

  1. Check MX validator logs for rejections:

    kubectl logs -n email-relay -l app=email-relay -c mx-validator | grep -i rejected
  2. Check smtp2graph logs for Graph API errors:

    kubectl logs -n email-relay -l app=email-relay -c smtp2graph | tail -50
  3. Verify Azure App Registration:

    • Mail.Send permission granted
    • Admin consent given
    • Client secret not expired
    • Correct tenant ID

MX validation rejecting valid domains

The MX cache might be stale. Restart the pod to clear:

kubectl delete pod -n email-relay -l app=email-relay

Or check DNS resolution from the pod:

kubectl exec -n email-relay deployment/email-relay -c mx-validator -- \
nslookup -type=MX gmail.com

Mailpit not showing emails

Check if mx-validator is forwarding to Mailpit:

kubectl logs -n email-relay -l app=email-relay -c mx-validator | grep -i accepted

Check Mailpit SMTP listener:

kubectl exec -n email-relay deployment/email-relay -c mailpit -- \
wget -q -O- http://localhost:8025/api/v1/info

smtp2graph authentication errors

Check logs for OAuth errors:

kubectl logs -n email-relay -l app=email-relay -c smtp2graph | grep -i error

Common issues:

  • "AADSTS700016": Client ID not found - verify the Application ID
  • "AADSTS7000215": Invalid client secret - regenerate in Azure portal
  • "AADSTS65001": No admin consent - grant consent in Azure portal

Pod stuck in CrashLoopBackOff

Check which container is failing:

kubectl describe pod -n email-relay -l app=email-relay

Check individual container logs:

kubectl logs -n email-relay -l app=email-relay -c mx-validator --previous
kubectl logs -n email-relay -l app=email-relay -c mailpit --previous
kubectl logs -n email-relay -l app=email-relay -c smtp2graph --previous

Force reconciliation

# Reconcile the source
flux reconcile source git email-relay -n flux-system

# Reconcile the app Kustomization
flux reconcile kustomization email-relay -n flux-system --with-source

Client secret rotation

When the Azure client secret expires (after 24 months):

  1. Create new secret in Azure portal
  2. Update the SOPS-encrypted secret:
    sops k8s/prod/15-secret-smtp2graph-config.enc.yaml
    # Update the secret value
  3. Commit and push
  4. Flux will reconcile and restart the pod

Using the relay from applications

Configure your application to use the relay:

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 including email verification for spam protection.