feat(mail): Z-Push Exchange ActiveSync gateway for mobile clients
Wraps Stalwart in EAS so iOS/Android native Mail/Calendar 'Exchange' accounts get two-way mail+calendar+contacts sync (BackendCombined: IMAP + CalDAV /dav/cal/%l/ + CardDAV, credentials pass through). - services/zpush: Z-Push 2.6.4 (AGPLv3, see LICENSE-NOTES.md) on php:8.2-apache-bookworm (trixie dropped libc-client); PHP 8 sysv sprintf fatal sed-patched; autodiscover dispatcher answers mobilesync schema, proxies outlook schema to Stalwart unchanged - prod: zpush Deployment (replicas:1, Recreate — file sync state), /Microsoft-Server-ActiveSync Ingress on mail.dezky.eu (no redirect, POST-heavy), autodiscover.dezky.eu repointed to the dispatcher, selectorless stalwart-imaps/-smtps Services (host-Stalwart is implicit-TLS only: 993/465, no plain 143/587 — verified on node1) - CI: build+deploy zpush like the other apps EAS tops out at 14.1: covers native mobile clients, NOT the Outlook mobile app (needs 16.1) and not new Outlook for Windows (no EAS).
This commit is contained in:
@@ -8,6 +8,7 @@ resources:
|
||||
- namespace.yaml
|
||||
- redirect-middleware.yaml
|
||||
- mail-autodiscovery.yaml
|
||||
- zpush.yaml
|
||||
- platform-api-config.yaml
|
||||
- platform-api.yaml
|
||||
- portal.yaml
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# Mail-client autodiscovery for dezky.eu — routes ONLY the discovery paths
|
||||
# through Traefik to host-Stalwart's HTTP listener (10.42.0.1:8080):
|
||||
# Mail-client autodiscovery for dezky.eu — routes ONLY the discovery paths:
|
||||
#
|
||||
# autodiscover.dezky.eu POST /autodiscover/autodiscover.xml (Outlook)
|
||||
# autodiscover.dezky.eu POST /autodiscover/autodiscover.xml (Outlook + EAS)
|
||||
# autoconfig.dezky.eu GET /mail/config-v1.1.xml (Thunderbird)
|
||||
#
|
||||
# autodiscover.dezky.eu goes to the zpush dispatcher (zpush.yaml), which
|
||||
# answers mobilesync-schema requests itself (Exchange ActiveSync devices)
|
||||
# and proxies outlook-schema (mail) requests through to host-Stalwart's
|
||||
# HTTP listener unchanged. autoconfig.dezky.eu still hits Stalwart directly.
|
||||
#
|
||||
# Everything else on these hostnames (Stalwart's /admin, /login, /jmap …)
|
||||
# falls through to Traefik's 404 — the management surface stays internal.
|
||||
# No HTTPS-redirect middleware on purpose: Thunderbird probes plain HTTP and
|
||||
@@ -44,6 +48,59 @@ subsets:
|
||||
- name: http
|
||||
port: 8080
|
||||
---
|
||||
# IMAPS + SMTPS for the zpush EAS gateway (zpush.yaml) — same
|
||||
# selectorless-Service-to-host pattern as stalwart-http above. Host-Stalwart
|
||||
# exposes ONLY the implicit-TLS variants (993/465; no plain 143/587 —
|
||||
# verified on node1 2026-06-12), all bound to the host wildcard, so the
|
||||
# cni0 gateway address reaches them just like :8080.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: stalwart-imaps
|
||||
labels:
|
||||
app.kubernetes.io/name: stalwart-imaps
|
||||
app.kubernetes.io/part-of: dezky
|
||||
spec:
|
||||
ports:
|
||||
- name: imaps
|
||||
port: 993
|
||||
targetPort: 993
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: stalwart-imaps
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 10.42.0.1
|
||||
ports:
|
||||
- name: imaps
|
||||
port: 993
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: stalwart-smtps
|
||||
labels:
|
||||
app.kubernetes.io/name: stalwart-smtps
|
||||
app.kubernetes.io/part-of: dezky
|
||||
spec:
|
||||
ports:
|
||||
- name: smtps
|
||||
port: 465
|
||||
targetPort: 465
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: stalwart-smtps
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 10.42.0.1
|
||||
ports:
|
||||
- name: smtps
|
||||
port: 465
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
@@ -62,13 +119,14 @@ spec:
|
||||
- host: autodiscover.dezky.eu
|
||||
http:
|
||||
paths:
|
||||
# Outlook probes both capitalizations.
|
||||
# Outlook probes both capitalizations. zpush dispatches by schema:
|
||||
# mobilesync → Z-Push, outlook (mail) → proxied to Stalwart.
|
||||
- path: /autodiscover/autodiscover.xml
|
||||
pathType: Exact
|
||||
backend: { service: { name: stalwart-http, port: { number: 8080 } } }
|
||||
backend: { service: { name: zpush, port: { number: 80 } } }
|
||||
- path: /Autodiscover/Autodiscover.xml
|
||||
pathType: Exact
|
||||
backend: { service: { name: stalwart-http, port: { number: 8080 } } }
|
||||
backend: { service: { name: zpush, port: { number: 80 } } }
|
||||
- host: autoconfig.dezky.eu
|
||||
http:
|
||||
paths:
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# zpush — Exchange ActiveSync gateway (Z-Push, AGPLv3) in front of host-
|
||||
# Stalwart. Gives "Exchange" accounts on iOS/Android native Mail/Calendar
|
||||
# two-way mail+calendar+contacts sync. EAS 14.1: covers native mobile
|
||||
# clients, NOT the Outlook mobile app (requires EAS 16.1) and not new
|
||||
# Outlook for Windows (no EAS at all). Credentials pass through to Stalwart
|
||||
# per-request (mailbox password or app password) — the pod stores only sync
|
||||
# state, which is disposable (devices resync after a wipe).
|
||||
#
|
||||
# replicas MUST stay 1: Z-Push's FILE state machine is not multi-writer-safe
|
||||
# (same constraint family as the portal/operator session pinning).
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: zpush-state
|
||||
namespace: dezky-apps
|
||||
labels:
|
||||
app.kubernetes.io/name: zpush
|
||||
app.kubernetes.io/part-of: dezky
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: zpush
|
||||
namespace: dezky-apps
|
||||
labels:
|
||||
app.kubernetes.io/name: zpush
|
||||
app.kubernetes.io/part-of: dezky
|
||||
spec:
|
||||
replicas: 1
|
||||
# Recreate, not RollingUpdate: the state PVC is RWO and the state machine
|
||||
# must never have two writers.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: zpush
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: zpush
|
||||
spec:
|
||||
containers:
|
||||
- name: zpush
|
||||
# CI pins this to the commit SHA at deploy time; :latest is the fallback.
|
||||
image: git.lastcloud.io/ronnibaslund/dezky/zpush:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
env:
|
||||
# Host-Stalwart reached via the selectorless Services pinning the
|
||||
# cni0 gateway address (see mail-autodiscovery.yaml).
|
||||
- name: CALDAV_SERVER
|
||||
value: stalwart-http.dezky-apps.svc.cluster.local
|
||||
- name: CALDAV_PORT
|
||||
value: "8080"
|
||||
# Host-Stalwart only exposes implicit-TLS IMAPS/SMTPS (no plain
|
||||
# 143/587) — hence /ssl with novalidate-cert (the cert names
|
||||
# mail.dezky.eu, not the cluster service) and the ssl:// prefix.
|
||||
- name: IMAP_SERVER
|
||||
value: stalwart-imaps.dezky-apps.svc.cluster.local
|
||||
- name: IMAP_PORT
|
||||
value: "993"
|
||||
- name: IMAP_OPTIONS
|
||||
value: /ssl/novalidate-cert
|
||||
- name: SMTP_SERVER
|
||||
value: ssl://stalwart-smtps.dezky-apps.svc.cluster.local
|
||||
- name: SMTP_PORT
|
||||
value: "465"
|
||||
# Hostname EAS autodiscover responses point devices at.
|
||||
- name: ZPUSH_HOST
|
||||
value: mail.dezky.eu
|
||||
# Outlook-schema (mail) autodiscover POSTs proxied to Stalwart.
|
||||
- name: MAIL_AUTODISCOVER_UPSTREAM
|
||||
value: http://stalwart-http.dezky-apps.svc.cluster.local:8080
|
||||
volumeMounts:
|
||||
- name: state
|
||||
mountPath: /var/lib/z-push
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 192Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- name: state
|
||||
persistentVolumeClaim:
|
||||
claimName: zpush-state
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: zpush
|
||||
namespace: dezky-apps
|
||||
labels:
|
||||
app.kubernetes.io/name: zpush
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: zpush
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
---
|
||||
# EAS endpoint on mail.dezky.eu. Path-scoped: everything else on the host
|
||||
# (CalDAV well-knowns, /dav) keeps routing to Stalwart via the mail-dav
|
||||
# Ingress. NO redirect middleware — EAS is POST-heavy and devices don't
|
||||
# follow 301s (same reasoning as the autodiscover Ingress). No cert-manager
|
||||
# annotation either: the mail-dav Ingress already owns the Certificate that
|
||||
# keeps mail-dezky-eu-traefik-tls fresh; we only reference the secret.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: zpush-eas
|
||||
namespace: dezky-apps
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- mail.dezky.eu
|
||||
secretName: mail-dezky-eu-traefik-tls
|
||||
rules:
|
||||
- host: mail.dezky.eu
|
||||
http:
|
||||
paths:
|
||||
- path: /Microsoft-Server-ActiveSync
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zpush
|
||||
port:
|
||||
number: 80
|
||||
Reference in New Issue
Block a user