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¶
- Go to Azure Portal > App Registrations
- Create a new registration or use existing
- Note the Application (client) ID and Directory (tenant) ID
- Under Certificates & secrets, create a new client secret
- Under API permissions, add
Microsoft Graph > Application > Mail.Send - 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¶
- Application sends email via SMTP to the relay service
- Relay receives the email on port 25
- Relay authenticates with Azure AD using client credentials flow
- Relay calls Microsoft Graph API
/users/{sender}/sendMail - 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¶
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.