From c83c9c796040b2b8022dde0f00b645fa38d60fc4 Mon Sep 17 00:00:00 2001 From: Levente Tamas Date: Wed, 7 Nov 2018 14:31:10 +0200 Subject: [PATCH] Add support for ActiveSync discovery Supports Windows 10 Mail and Android Exchange accounts --- src/autoconfig.php | 279 ++++++++++++++++++++--------- src/autoconfig.settings.sample.php | 7 +- 2 files changed, 201 insertions(+), 85 deletions(-) diff --git a/src/autoconfig.php b/src/autoconfig.php index fa081c1..e794561 100644 --- a/src/autoconfig.php +++ b/src/autoconfig.php @@ -2,21 +2,21 @@ class Configuration { private $items = array(); - + public function add($id) { $result = new DomainConfiguration(); $result->id = $id; array_push($this->items, $result); return $result; } - + public function getDomainConfig($domain) { foreach ($this->items as $domainConfig) { if (in_array($domain, $domainConfig->domains)) { return $domainConfig; } } - + throw new Exception('No configuration found for requested domain.'); } } @@ -25,14 +25,14 @@ class DomainConfiguration { public $domains; public $servers = array(); public $username; - + public function addServer($type, $hostname) { $server = $this->createServer($type, $hostname); $server->username = $this->username; array_push($this->servers, $server); return $server; } - + private function createServer($type, $hostname) { switch ($type) { case 'imap': @@ -41,6 +41,8 @@ private function createServer($type, $hostname) { return new Server($type, $hostname, 110, 995); case 'smtp': return new Server($type, $hostname, 25, 465); + case 'activesync': + return new Server($type, $hostname, 80, 443); default: throw new Exception("Unrecognized server type \"$type\""); } @@ -53,7 +55,7 @@ class Server { public $username; public $endpoints; public $samePassword; - + public function __construct($type, $hostname, $defaultPort, $defaultSslPort) { $this->type = $type; $this->hostname = $hostname; @@ -62,31 +64,29 @@ public function __construct($type, $hostname, $defaultPort, $defaultSslPort) { $this->endpoints = array(); $this->samePassword = true; } - + public function withUsername($username) { $this->username = $username; return $this; } - + public function withDifferentPassword() { $this->samePassword = false; return $this; } - + public function withEndpoint($socketType, $port = null, $authentication = 'password-cleartext') { if ($port === null) { $port = $socketType === 'SSL' ? $this->defaultSslPort : $this->defaultPort; } - + array_push($this->endpoints, (object)array( 'socketType' => $socketType, 'port' => $port, 'authentication' => $authentication)); - + return $this; } - - } interface UsernameResolver { @@ -95,41 +95,41 @@ public function findUsername($request); class AliasesFileUsernameResolver implements UsernameResolver { private $fileName; - + function __construct($fileName = "/etc/mail/aliases") { $this->fileName = $fileName; } - + public function findUsername($request) { static $cachedEmail = null; static $cachedUsername = null; - + if ($request->email === $cachedEmail) { return $cachedUsername; } - + $fp = fopen($this->fileName, 'rb'); - + if ($fp === false) { throw new Exception("Unable to open aliases file \"$fileName\""); } - + $username = $this->findLocalPart($fp, $request->localpart); if (strpos($username, "@") !== false || strpos($username, ",") !== false) { $username = null; } - + $cachedEmail = $request->email; $cachedUsername = $username; - return $username; + return $username; } - + protected function findLocalPart($fp, $localPart) { while (($line = fgets($fp)) !== false) { $matches = array(); if (!preg_match("/^\s*" . preg_quote($localPart) . "\s*:\s*(\S+)\s*$/", $line, $matches)) continue; return $matches[1]; - } + } } } @@ -140,13 +140,13 @@ public function handleRequest() { $config = $this->getDomainConfig($request); $this->writeResponse($config, $request); } - + protected abstract function parseRequest(); protected abstract function writeResponse($config, $request); - + protected function expandRequest($request) { list($localpart, $domain) = explode('@', $request->email); - + if (!isset($request->localpart)) { $request->localpart = $localpart; } @@ -155,36 +155,36 @@ protected function expandRequest($request) { $request->domain = strtolower($domain); } } - + protected function getDomainConfig($request) { static $cachedEmail = null; static $cachedConfig = null; - + if ($cachedEmail === $request->email) { return $cachedConfig; } - + $cachedConfig = $this->readConfig($request); $cachedEmail = $request->email; - + return $cachedConfig->getDomainConfig($request->domain); } - + protected function readConfig($vars) { foreach ($vars as $var => $value) { $$var = $value; } - + $config = new Configuration(); include './autoconfig.settings.php'; return $config; } - + protected function getUsername($server, $request) { if (is_string($server->username)) { return $server->username; } - + if ($server->username instanceof UsernameResolver) { $resolver = $server->username; return $resolver->findUsername($request); @@ -197,47 +197,47 @@ public function writeResponse($config, $request) { header("Content-Type: text/xml"); $writer = new XMLWriter(); $writer->openURI("php://output"); - + $this->writeXml($writer, $config, $request); $writer->flush(); } - + protected function parseRequest() { return (object)array('email' => $_GET['emailaddress']); } - + protected function writeXml($writer, $config, $request) { $writer->startDocument("1.0"); $writer->setIndent(4); $writer->startElement("clientConfig"); $writer->writeAttribute("version", "1.1"); - + $this->writeEmailProvider($writer, $config, $request); - + $writer->endElement(); $writer->endDocument(); - } - + } + protected function writeEmailProvider($writer, $config, $request) { $writer->startElement("emailProvider"); $writer->writeAttribute("id", $config->id); - + foreach ($config->domains as $domain) { $writer->writeElement("domain", $domain); } - + $writer->writeElement("displayName", $config->name); $writer->writeElement("displayShortName", $config->nameShort); - + foreach ($config->servers as $server) { foreach ($server->endpoints as $endpoint) { $this->writeServer($writer, $server, $endpoint, $request); } } - + $writer->endElement(); } - + protected function writeServer($writer, $server, $endpoint, $request) { switch ($server->type) { case 'imap': @@ -249,11 +249,11 @@ protected function writeServer($writer, $server, $endpoint, $request) { break; } } - + protected function writeIncomingServer($writer, $server, $endpoint, $request) { $authentication = $this->mapAuthenticationType($endpoint->authentication); if (empty($authentication)) return; - + $writer->startElement("incomingServer"); $writer->writeAttribute("type", $server->type); $writer->writeElement("hostname", $server->hostname); @@ -263,27 +263,27 @@ protected function writeIncomingServer($writer, $server, $endpoint, $request) { $writer->writeElement("authentication", $authentication); $writer->endElement(); } - + protected function writeSmtpServer($writer, $server, $endpoint, $request) { $authentication = $this->mapAuthenticationType($endpoint->authentication); if ($authentication === null) return; - + $writer->startElement("outgoingServer"); $writer->writeAttribute("type", "smtp"); $writer->writeElement("hostname", $server->hostname); $writer->writeElement("port", $endpoint->port); $writer->writeElement("socketType", $endpoint->socketType); - + if ($authentication !== false) { $writer->writeElement("username", $this->getUsername($server, $request)); $writer->writeElement("authentication", $authentication); } - + $writer->writeElement("addThisServer", "true"); $writer->writeElement("useGlobalPreferredServer", "true"); $writer->endElement(); } - + protected function mapAuthenticationType($authentication) { switch ($authentication) { case 'password-cleartext': @@ -294,34 +294,40 @@ protected function mapAuthenticationType($authentication) { return false; default: return null; - } + } } } class OutlookHandler extends RequestHandler { + public $postdata; + + function __construct($data) { + $this->postdata=$data; + } + public function writeResponse($config, $request) { header("Content-Type: application/xml"); - + $writer = new XMLWriter(); $writer->openMemory(); - + $this->writeXml($writer, $config, $request); - + $response = $writer->outputMemory(true); echo $response; } - + protected function parseRequest() { - $postdata = file_get_contents("php://input"); - + $postdata = $self->postdata; + if (strlen($postdata) > 0) { $xml = simplexml_load_string($postdata); return (object)array('email' => $xml->Request->EMailAddress); } - + return null; } - + public function writeXml($writer, $config, $request) { $writer->startDocument("1.0", "utf-8"); $writer->setIndent(4); @@ -329,25 +335,25 @@ public function writeXml($writer, $config, $request) { $writer->writeAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"); $writer->startElement("Response"); $writer->writeAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"); - + $writer->startElement("Account"); $writer->writeElement("AccountType", "email"); $writer->writeElement("Action", "settings"); - + foreach ($config->servers as $server) { foreach ($server->endpoints as $endpoint) { if ($this->writeProtocol($writer, $server, $endpoint, $request)) break; } - } - + } + $writer->endElement(); - + $writer->endElement(); $writer->endElement(); $writer->endDocument(); } - + protected function writeProtocol($writer, $server, $endpoint, $request) { switch ($endpoint->authentication) { case 'password-cleartext': @@ -359,7 +365,7 @@ protected function writeProtocol($writer, $server, $endpoint, $request) { default: return false; } - + $writer->startElement('Protocol'); $writer->writeElement('Type', strtoupper($server->type)); $writer->writeElement('Server', $server->hostname); @@ -367,7 +373,7 @@ protected function writeProtocol($writer, $server, $endpoint, $request) { $writer->writeElement('LoginName', $this->getUsername($server, $request)); $writer->writeElement('DomainRequired', 'off'); $writer->writeElement('SPA', $endpoint->authentication === 'SPA' ? 'on' : 'off'); - + switch ($endpoint->socketType) { case 'plain': $writer->writeElement("SSL", "off"); @@ -381,19 +387,19 @@ protected function writeProtocol($writer, $server, $endpoint, $request) { $writer->writeElement("Encryption", "TLS"); break; } - + $writer->writeElement("AuthRequired", $endpoint->authentication !== 'none' ? 'on' : 'off'); - + if ($server->type == 'smtp') { $writer->writeElement('UsePOPAuth', $server->samePassword ? 'on' : 'off'); $writer->writeElement('SMTPLast', 'off'); } - + $writer->endElement(); - + return true; } - + protected function mapAuthenticationType($authentication) { switch ($authentication) { case 'password-cleartext': @@ -404,17 +410,124 @@ protected function mapAuthenticationType($authentication) { return false; default: return null; - } - } - + } + } + } -if (strpos($_SERVER['SERVER_NAME'], "autoconfig.") === 0) { +class ActiveSyncHandler extends RequestHandler { + public $postdata; + + function __construct($data) { + $this->postdata=$data; + } + + public function writeResponse($config, $request) { + header("Content-Type: application/xml"); + + $writer = new XMLWriter(); + $writer->openMemory(); + + $this->writeXml($writer, $config, $request); + + $response = $writer->outputMemory(true); + echo $response; + } + + protected function parseRequest() { + $postdata = $this->postdata; + + if (strlen($postdata) > 0) { + $xml = simplexml_load_string($postdata); + return (object)array('email' => $xml->Request->EMailAddress); + } + + return null; + } + + public function writeXml($writer, $config, $request) { + $writer->startDocument("1.0", "utf-8"); + $writer->setIndent(4); + $writer->startElement("Autodiscover"); + $writer->writeAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"); + $writer->writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); + $writer->writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + + $writer->startElement("Response"); + + $writer->writeElement("Culture",'en:us'); + + $writer->startElement("User"); + $writer->writeElement("DisplayName", 'Offcode' /*$config->displayName*/); + $writer->writeElement("EMailAddress", $request->email); + $writer->endElement(); // User + + $writer->startElement("Action"); + $writer->startElement("Settings"); + + foreach ($config->servers as $server) { + foreach ($server->endpoints as $endpoint) { + if ($this->writeProtocol($writer, $server, $endpoint, $request)) + break; + } + } + + $writer->endElement(); //Settings + $writer->endElement(); //Action + $writer->endElement(); //Response + $writer->endElement(); //Autodiscover + $writer->endDocument(); + } + + protected function writeProtocol($writer, $server, $endpoint, $request) { + if ($server->type !== 'activesync') return false; + + $writer->startElement('Server'); + $writer->writeElement('Type', 'MobileSync'); + + switch ($endpoint->socketType) { + case 'http': + $url='http://'; + $suffix=($endpoint->port == 80)?'':(':'.$endpoint->port); + break; + case 'https': + $url='https://'; + $suffix=($endpoint->port == 443)?'':(':'.$endpoint->port); + break; + } + $url.=$server->hostname.$suffix.'/Microsoft-Server-ActiveSync'; + + $writer->writeElement('Url', $url); + $writer->writeElement('Name', $url); + + $writer->endElement(); + + return true; + } + + protected function mapAuthenticationType($authentication) { + return null; + } + +} + + +if (strpos($_SERVER['HTTP_HOST'], "autoconfig.") === 0) { // Configuration for Mozilla Thunderbird, Evolution, KMail, Kontact $handler = new MozillaHandler(); -} else if (strpos($_SERVER['SERVER_NAME'], "autodiscover.") === 0) { - // Configuration for Outlook - $handler = new OutlookHandler(); +} else if (strpos($_SERVER['HTTP_HOST'], "autodiscover.") === 0) { + $body=file_get_contents("php://input"); + file_put_contents('indata',$body); + if (strlen($body) > 0) { + $xml = simplexml_load_string($body); + if ($xml->Request->AcceptableResponseSchema=="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006") { + // Configuration for Windows 10 Mail + $handler = new ActiveSyncHandler($body); + } else { + // Configuration for Outlook + $handler = new OutlookHandler($body); + } + } } -$handler->handleRequest(); +$handler->handleRequest(); \ No newline at end of file diff --git a/src/autoconfig.settings.sample.php b/src/autoconfig.settings.sample.php index 90e4b10..79b5f97 100644 --- a/src/autoconfig.settings.sample.php +++ b/src/autoconfig.settings.sample.php @@ -48,7 +48,7 @@ $cfg->addServer('imap', 'imap.example.com') ->withEndpoint('STARTTLS') ->withEndpoint('SSL'); - + // Example POP3 server for incoming mail, running on port 110 (TLS) and 995 (SSL) $cfg->addServer('pop3', 'pop.example.com') ->withEndpoint('STARTTLS') @@ -58,4 +58,7 @@ $cfg->addServer('smtp', 'smtp.example.com') ->withEndpoint('STARTTLS', 587) ->withEndpoint('SSL'); - + +// Example ActiveSync server +$cfg->addServer('activesync', 'mail.example.com') + ->withEndpoint('https', 443);