Skip to content

Microsoft Graph OAuth2 SMTP Relay

The homelab uses a custom SMTP relay service that authenticates via Microsoft Graph API to send emails through Microsoft 365, eliminating the need for app passwords or SMTP AUTH.

Overview

Property Value
Namespace graph-smtp-relay
Internal Service graph-smtp-relay.graph-smtp-relay.svc:25
Authentication OAuth2 Client Credentials Flow
Graph API Microsoft Graph v1.0

Architecture

graph LR
    subgraph Kubernetes
        NC[Nextcloud]
        HUB[Hub API]
        OTHER[Other Apps]
        RELAY[Graph SMTP Relay<br/>:25]
    end

    subgraph Microsoft
        AAD[Azure AD]
        GRAPH[Graph API]
        M365[Microsoft 365]
    end

    subgraph Recipients
        USER[Email Recipients]
    end

    NC -->|SMTP| RELAY
    HUB -->|SMTP| RELAY
    OTHER -->|SMTP| RELAY
    RELAY -->|OAuth2 Token| AAD
    RELAY -->|/sendMail| GRAPH
    GRAPH --> M365
    M365 --> USER

Azure AD App Registration

Required Permissions

The Azure AD application requires the following Application permissions (not Delegated):

Permission Type Description
Mail.Send Application Send mail as any user

Admin Consent Required

Application permissions require Azure AD admin consent to be granted.

Configuration

  1. Go to Azure Portal > App Registrations
  2. Create a new registration or use existing
  3. Note the Application (client) ID and Directory (tenant) ID
  4. Under Certificates & secrets, create a new client secret
  5. Under API permissions, add Microsoft Graph > Application > Mail.Send
  6. Grant admin consent

Kubernetes Deployment

Secret

apiVersion: v1
kind: Secret
metadata:
  name: graph-smtp-secrets
  namespace: graph-smtp-relay
type: Opaque
stringData:
  AZURE_TENANT_ID: "your-tenant-id"
  AZURE_CLIENT_ID: "your-client-id"
  AZURE_CLIENT_SECRET: "your-client-secret"
  SENDER_EMAIL: "[email protected]"

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: graph-smtp-relay
  namespace: graph-smtp-relay
spec:
  replicas: 1
  selector:
    matchLabels:
      app: graph-smtp-relay
  template:
    metadata:
      labels:
        app: graph-smtp-relay
    spec:
      containers:
      - name: relay
        image: ghcr.io/ajandrews51/graph-smtp-relay:latest
        ports:
        - containerPort: 25
          name: smtp
        - containerPort: 8080
          name: health
        env:
        - name: AZURE_TENANT_ID
          valueFrom:
            secretKeyRef:
              name: graph-smtp-secrets
              key: AZURE_TENANT_ID
        - name: AZURE_CLIENT_ID
          valueFrom:
            secretKeyRef:
              name: graph-smtp-secrets
              key: AZURE_CLIENT_ID
        - name: AZURE_CLIENT_SECRET
          valueFrom:
            secretKeyRef:
              name: graph-smtp-secrets
              key: AZURE_CLIENT_SECRET
        - name: SENDER_EMAIL
          valueFrom:
            secretKeyRef:
              name: graph-smtp-secrets
              key: SENDER_EMAIL
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
          limits:
            cpu: 200m
            memory: 128Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

Service

apiVersion: v1
kind: Service
metadata:
  name: graph-smtp-relay
  namespace: graph-smtp-relay
spec:
  selector:
    app: graph-smtp-relay
  ports:
  - port: 25
    targetPort: 25
    name: smtp
  - port: 8080
    targetPort: 8080
    name: health

Client Configuration

Nextcloud

Configure in Nextcloud's config.php or via environment variables:

env:
- name: SMTP_HOST
  value: "graph-smtp-relay.graph-smtp-relay.svc"
- name: SMTP_PORT
  value: "25"
- name: SMTP_SECURE
  value: ""  # No TLS for internal cluster communication
- name: SMTP_AUTHTYPE
  value: ""  # No auth needed - relay handles it
- name: MAIL_FROM_ADDRESS
  value: "cloud"
- name: MAIL_DOMAIN
  value: "ajandrews.pro"

Generic Application

For any application that supports SMTP:

Setting Value
SMTP Host graph-smtp-relay.graph-smtp-relay.svc
SMTP Port 25
TLS/SSL None (internal cluster)
Authentication None
From Address Your Microsoft 365 sender email

How It Works

  1. Application sends email via SMTP to the relay service
  2. Relay receives the email on port 25
  3. Relay authenticates with Azure AD using client credentials flow
  4. Relay calls Microsoft Graph API /users/{sender}/sendMail
  5. Microsoft 365 delivers the email to recipients

Token Management

  • OAuth2 tokens are cached in memory
  • Automatic refresh before expiration
  • No manual token management required

Monitoring

Health Endpoints

Endpoint Purpose
GET /health Liveness check
GET /ready Readiness check (includes token validation)
GET /metrics Prometheus metrics

Prometheus Metrics

# Emails sent successfully
graph_smtp_emails_sent_total

# Emails failed
graph_smtp_emails_failed_total

# Token refresh count
graph_smtp_token_refreshes_total

# Current token expiry timestamp
graph_smtp_token_expiry_timestamp

Troubleshooting

Test Email Sending

# Port forward to the relay
kubectl port-forward -n graph-smtp-relay svc/graph-smtp-relay 2525:25

# Send test email using telnet or swaks
swaks --to [email protected] \
      --from [email protected] \
      --server localhost:2525 \
      --body "Test email from Graph SMTP Relay"

Check Logs

kubectl logs -n graph-smtp-relay deployment/graph-smtp-relay

Common Issues

Issue Cause Solution
401 Unauthorized Invalid credentials Verify Azure AD app credentials
403 Forbidden Missing Mail.Send permission Grant admin consent
Connection refused Service not running Check pod status
Emails not delivered Sender not authorized Verify sender email is licensed M365 user

Security Considerations

  • Client secret should be rotated periodically
  • Use Kubernetes secrets (consider sealed-secrets or external secrets)
  • Internal service only - not exposed externally
  • Audit logs available in Azure AD
  • Rate limits apply (Graph API limits)

Repository

Managed via ArgoCD from the main infrastructure repository.