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:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
// dezky autodiscover dispatcher. EAS devices and Outlook's mail setup POST
|
||||
// the same /autodiscover/autodiscover.xml URL but request different response
|
||||
// schemas — and Z-Push's autodiscover only answers the mobilesync schema
|
||||
// (it 400s the outlook schema), while Stalwart only serves the mail one.
|
||||
// So: sniff the request body and route mobilesync to Z-Push, everything
|
||||
// else to Stalwart unchanged. Deployed into /usr/share/z-push/autodiscover/
|
||||
// because autodiscover.php resolves its requires relative to that directory.
|
||||
|
||||
$body = file_get_contents('php://input');
|
||||
|
||||
if (stripos($body, 'autodiscover/mobilesync/responseschema') !== false) {
|
||||
chdir(__DIR__);
|
||||
require __DIR__ . '/autodiscover.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$upstream = getenv('MAIL_AUTODISCOVER_UPSTREAM');
|
||||
if ($upstream === false || $upstream === '') {
|
||||
http_response_code(502);
|
||||
header('Content-Type: text/plain');
|
||||
echo "autodiscover upstream not configured\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Forward the raw Authorization header when present (SetEnvIf in zpush.conf
|
||||
// exposes it); fall back to rebuilding Basic from mod_php's decoded pair.
|
||||
$headers = array('Content-Type: ' . ($_SERVER['CONTENT_TYPE'] ?? 'text/xml'));
|
||||
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
$headers[] = 'Authorization: ' . $_SERVER['HTTP_AUTHORIZATION'];
|
||||
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$headers[] = 'Authorization: Basic '
|
||||
. base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . ($_SERVER['PHP_AUTH_PW'] ?? ''));
|
||||
}
|
||||
|
||||
$ch = curl_init(rtrim($upstream, '/') . '/autodiscover/autodiscover.xml');
|
||||
curl_setopt_array($ch, array(
|
||||
CURLOPT_CUSTOMREQUEST => $_SERVER['REQUEST_METHOD'],
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
));
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
http_response_code(502);
|
||||
header('Content-Type: text/plain');
|
||||
echo "autodiscover upstream unreachable\n";
|
||||
curl_close($ch);
|
||||
exit;
|
||||
}
|
||||
|
||||
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
http_response_code($status ?: 502);
|
||||
header('Content-Type: ' . ($contentType ?: 'application/xml'));
|
||||
echo $response;
|
||||
Reference in New Issue
Block a user